diff --git a/docs/api/index.md b/docs/api/index.md index 8441d1f2a..63f94df13 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -45,6 +45,7 @@ test('mounts a component', () => { Notice that `mount` accepts a second parameter to define the component's state configuration. **Example: mounting with component props and a Vue App plugin** + ```js const wrapper = mount(Component, { props: { @@ -374,9 +375,7 @@ export default { ```vue ``` @@ -1079,10 +1078,10 @@ import { mount } from '@vue/test-utils' import BaseTable from './BaseTable.vue' test('findAll', () => { - const wrapper = mount(BaseTable); + const wrapper = mount(BaseTable) // .findAll() returns an array of DOMWrappers - const thirdRow = wrapper.findAll('span')[2]; + const thirdRow = wrapper.findAll('span')[2] }) ``` @@ -1166,21 +1165,6 @@ test('findComponent', () => { If `ref` in component points to HTML element, `findComponent` will return empty wrapper. This is intended behaviour ::: - -**NOTE** `getComponent` and `findComponent` will not work on functional components, because they do not have an internal Vue instance (this is what makes functional components more performant). That means the following will **not** work: - -```js -const Foo = () => h('div') - -const wrapper = mount(Foo) -// doesn't work! You get a wrapper, but since there is not -// associated Vue instance, you cannot use methods like -// exists() and text() -wrapper.findComponent(Foo) -``` - -For tests using functional component, consider using `get` or `find` and treating them like standard DOM nodes. - :::warning Usage with CSS selectors Using `findComponent` with CSS selector might have confusing behavior @@ -1489,20 +1473,6 @@ test('props', () => { }) ``` -**NOTE** `getComponent` and `findComponent` will not work on functional components, because they do not have an internal Vue instance (this is what makes functional components more performant). That means the following will **not** work: - -```js -const Foo = () => h('div') - -const wrapper = mount(Foo) -// doesn't work! You get a wrapper, but since there is not -// associated Vue instance, you cannot use methods like -// exists() and text() -wrapper.findComponent(Foo) -``` - -For tests using functional component, consider using `get` or `find` and treating them like standard DOM nodes. - :::tip As a rule of thumb, test against the effects of a passed prop (a DOM update, an emitted event, and so on). This will make tests more powerful than simply asserting that a prop is passed. ::: @@ -1862,8 +1832,8 @@ function shallowMount(Component, options?: MountingOptions): VueWrapper ## enableAutoUnmount - **Signature:** + ```ts enableAutoUnmount(hook: Function)); disableAutoUnmount(): void; diff --git a/src/baseWrapper.ts b/src/baseWrapper.ts index cb4f3b111..7bf6f3c70 100644 --- a/src/baseWrapper.ts +++ b/src/baseWrapper.ts @@ -3,15 +3,30 @@ import type { TriggerOptions } from './createDomEvent' import { ComponentInternalInstance, ComponentPublicInstance, + FunctionalComponent, nextTick } from 'vue' import { createDOMEvent } from './createDomEvent' import { DomEventNameWithModifier } from './constants/dom-events' import type { VueWrapper } from './vueWrapper' +import { + DefinedComponent, + FindAllComponentsSelector, + FindComponentSelector, + NameSelector, + RefSelector +} from './types' +import WrapperLike from './interfaces/wrapperLike' +import { find, matches } from './utils/find' +import { createWrapperError } from './errorWrapper' +import { isElementVisible } from './utils/isElementVisible' +import { isElement } from './utils/isElement' import type { DOMWrapper } from './domWrapper' -import { FindAllComponentsSelector, FindComponentSelector } from './types' +import { createDOMWrapper, createVueWrapper } from './wrapperFactory' -export default abstract class BaseWrapper { +export default abstract class BaseWrapper + implements WrapperLike +{ private readonly wrapperElement: ElementType & { __vueParentComponent?: ComponentInternalInstance } @@ -24,33 +39,170 @@ export default abstract class BaseWrapper { this.wrapperElement = element } - abstract find(selector: string): DOMWrapper - abstract findAll(selector: string): DOMWrapper[] - abstract findComponent( - selector: FindComponentSelector | (new () => T) + find( + selector: K + ): DOMWrapper + find( + selector: K + ): DOMWrapper + find(selector: string | RefSelector): DOMWrapper + find(selector: string | RefSelector): DOMWrapper + find(selector: string | RefSelector): DOMWrapper { + // allow finding the root element + if (!isElement(this.element)) { + return createWrapperError('DOMWrapper') + } + + if (typeof selector === 'object' && 'ref' in selector) { + const currentComponent = this.getCurrentComponent() + if (!currentComponent) { + return createWrapperError('DOMWrapper') + } + + const result = currentComponent.refs[selector.ref] + + if (result instanceof HTMLElement) { + return createDOMWrapper(result) + } else { + return createWrapperError('DOMWrapper') + } + } + + if (this.element.matches(selector)) { + return createDOMWrapper(this.element) + } + const result = this.element.querySelector(selector) + if (result) { + return createDOMWrapper(result) + } + + return createWrapperError('DOMWrapper') + } + + findAll( + selector: K + ): DOMWrapper[] + findAll( + selector: K + ): DOMWrapper[] + findAll(selector: string): DOMWrapper[] + findAll(selector: string): DOMWrapper[] { + if (!isElement(this.element)) { + return [] + } + + const result = this.element.matches(selector) + ? [createDOMWrapper(this.element)] + : [] + + return [ + ...result, + ...Array.from(this.element.querySelectorAll(selector)).map((x) => + createDOMWrapper(x) + ) + ] + } + + // searching by string without specifying component results in WrapperLike object + findComponent(selector: string): WrapperLike + // searching for component created via defineComponent results in VueWrapper of proper type + findComponent( + selector: T | Exclude + ): VueWrapper> + // searching for functional component results in DOMWrapper + findComponent( + selector: T | string + ): DOMWrapper + // searching by name or ref always results in VueWrapper + findComponent( + selector: NameSelector | RefSelector + ): VueWrapper + findComponent( + selector: T | FindComponentSelector ): VueWrapper - abstract findAllComponents( + // catch all declaration + findComponent(selector: FindComponentSelector): WrapperLike + + findComponent(selector: FindComponentSelector): WrapperLike { + const currentComponent = this.getCurrentComponent() + if (!currentComponent) { + return createWrapperError('VueWrapper') + } + + if (typeof selector === 'object' && 'ref' in selector) { + const result = currentComponent.refs[selector.ref] + if (result && !(result instanceof HTMLElement)) { + return createVueWrapper(null, result as ComponentPublicInstance) + } else { + return createWrapperError('VueWrapper') + } + } + + if ( + matches(currentComponent.vnode, selector) && + this.element.contains(currentComponent.vnode.el as Node) + ) { + return createVueWrapper(null, currentComponent.proxy!) + } + + const [result] = this.findAllComponents(selector) + return result ?? createWrapperError('VueWrapper') + } + + findAllComponents(selector: string): WrapperLike[] + findAllComponents( + selector: T | Exclude + ): VueWrapper>[] + findAllComponents( + selector: T | string + ): DOMWrapper[] + findAllComponents(selector: NameSelector): VueWrapper[] + findAllComponents( + selector: T | FindAllComponentsSelector + ): VueWrapper[] + // catch all declaration + findAllComponents( selector: FindAllComponentsSelector - ): VueWrapper[] + ): WrapperLike[] + + findAllComponents(selector: FindAllComponentsSelector): WrapperLike[] { + const currentComponent = this.getCurrentComponent() + if (!currentComponent) { + return [] + } + + let results = find(currentComponent.subTree, selector) + + return results.map((c) => + c.proxy + ? createVueWrapper(null, c.proxy) + : createDOMWrapper(c.vnode.el as Element) + ) + } + abstract setValue(value?: any): Promise abstract html(): string classes(): string[] classes(className: string): boolean classes(className?: string): string[] | boolean { - const classes = this.element.classList + const classes = isElement(this.element) + ? Array.from(this.element.classList) + : [] - if (className) return classes.contains(className) + if (className) return classes.includes(className) - return Array.from(classes) + return classes } attributes(): { [key: string]: string } attributes(key: string): string attributes(key?: string): { [key: string]: string } | string { - const attributes = Array.from(this.element.attributes) const attributeMap: Record = {} - for (const attribute of attributes) { - attributeMap[attribute.localName] = attribute.value + if (isElement(this.element)) { + const attributes = Array.from(this.element.attributes) + for (const attribute of attributes) { + attributeMap[attribute.localName] = attribute.value + } } return key ? attributeMap[key] : attributeMap @@ -80,13 +232,30 @@ export default abstract class BaseWrapper { throw new Error(`Unable to get ${selector} within: ${this.html()}`) } + getComponent(selector: string): Omit + getComponent( + selector: T | Exclude + ): Omit>, 'exists'> + // searching for functional component results in DOMWrapper + getComponent( + selector: T | string + ): Omit, 'exists'> + // searching by name or ref always results in VueWrapper + getComponent( + selector: NameSelector | RefSelector + ): Omit getComponent( - selector: FindComponentSelector | (new () => T) - ): Omit, 'exists'> { + selector: T | FindComponentSelector + ): Omit, 'exists'> + // catch all declaration + getComponent( + selector: FindComponentSelector + ): Omit + getComponent(selector: FindComponentSelector): Omit { const result = this.findComponent(selector) if (result.exists()) { - return result as VueWrapper + return result } let message = 'Unable to get ' @@ -116,13 +285,19 @@ export default abstract class BaseWrapper { 'INPUT' ] const hasDisabledAttribute = this.attributes().disabled !== undefined - const elementCanBeDisabled = validTagsToBeDisabled.includes( - this.element.tagName - ) + const elementCanBeDisabled = + isElement(this.element) && + validTagsToBeDisabled.includes(this.element.tagName) return hasDisabledAttribute && elementCanBeDisabled } + isVisible() { + return isElement(this.element) && isElementVisible(this.element) + } + + protected abstract getCurrentComponent(): ComponentInternalInstance | void + async trigger( eventString: DomEventNameWithModifier, options?: TriggerOptions diff --git a/src/config.ts b/src/config.ts index fc6dcc9bc..004e601cf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,3 @@ -import { ComponentPublicInstance } from 'vue' import { GlobalMountOptions } from './types' import { VueWrapper } from './vueWrapper' import { DOMWrapper } from './domWrapper' @@ -6,8 +5,8 @@ import { DOMWrapper } from './domWrapper' export interface GlobalConfigOptions { global: Required plugins: { - VueWrapper: Pluggable> - DOMWrapper: Pluggable> + VueWrapper: Pluggable + DOMWrapper: Pluggable> } renderStubDefaultSlot: boolean } diff --git a/src/domWrapper.ts b/src/domWrapper.ts index 5bccac4c9..605f184bc 100644 --- a/src/domWrapper.ts +++ b/src/domWrapper.ts @@ -1,110 +1,29 @@ import { config } from './config' -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 { matches, find } from './utils/find' -import type { createWrapper, VueWrapper } from './vueWrapper' - -export class DOMWrapper - extends BaseWrapper - implements WrapperLike -{ - constructor( - element: ElementType, - private createVueWrapper: typeof createWrapper - ) { +import { isElement } from './utils/isElement' +import { registerFactory, WrapperType } from './wrapperFactory' + +export class DOMWrapper extends BaseWrapper { + constructor(element: NodeType) { super(element) // plugins hook config.plugins.DOMWrapper.extend(this) } - isVisible() { - return isElementVisible(this.element) + getCurrentComponent() { + return this.element.__vueParentComponent } html() { - return this.element.outerHTML - } - - find( - selector: K - ): DOMWrapper - find( - selector: K - ): DOMWrapper - find(selector: string): DOMWrapper - find(selector: string): DOMWrapper { - // allow finding the root element - if (this.element.matches(selector)) { - return this - } - const result = this.element.querySelector(selector) - if (result) { - return new DOMWrapper(result, this.createVueWrapper) - } - - return createWrapperError('DOMWrapper') - } - - findAll( - selector: K - ): DOMWrapper[] - findAll( - selector: K - ): DOMWrapper[] - findAll(selector: string): DOMWrapper[] - findAll(selector: string): DOMWrapper[] { - return Array.from(this.element.querySelectorAll(selector)).map( - (x) => new DOMWrapper(x, this.createVueWrapper) - ) - } - - findComponent( - selector: FindComponentSelector | (new () => T) - ): VueWrapper { - 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 this.createVueWrapper(null, result as T) - } else { - return createWrapperError('VueWrapper') - } - } - - if ( - matches(parentComponent.vnode, selector) && - this.element.contains(parentComponent.vnode.el as Node) - ) { - return this.createVueWrapper(null, parentComponent.proxy!) - } - - const result = find(parentComponent.subTree, selector).filter((v) => - this.element.contains(v.$el) - ) - - if (result.length) { - return this.createVueWrapper(null, result[0]) - } - - return createWrapperError('VueWrapper') + return isElement(this.element) + ? this.element.outerHTML + : this.element.toString() } - 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) => this.createVueWrapper(null, c)) + findAllComponents(selector: any): any { + const results = super.findAllComponents(selector) + return results.filter((r: WrapperLike) => this.element.contains(r.element)) } private async setChecked(checked: boolean = true) { @@ -176,8 +95,8 @@ export class DOMWrapper parentElement = parentElement.parentElement! } - return new DOMWrapper(parentElement, this.createVueWrapper).trigger( - 'change' - ) + return new DOMWrapper(parentElement).trigger('change') } } + +registerFactory(WrapperType.DOMWrapper, (element) => new DOMWrapper(element)) diff --git a/src/index.ts b/src/index.ts index 1f9e8dc75..d765d64b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ +import { DOMWrapper } from './domWrapper' +import { VueWrapper } from './vueWrapper' import { mount, shallowMount } from './mount' import { MountingOptions } from './types' import { RouterLinkStub } from './components/RouterLinkStub' -import { VueWrapper } from './vueWrapper' -import { DOMWrapper } from './domWrapper' import { createWrapperError } from './errorWrapper' import { config } from './config' import { flushPromises } from './utils/flushPromises' diff --git a/src/interfaces/wrapperLike.ts b/src/interfaces/wrapperLike.ts index cc20ec26f..b5d4405d2 100644 --- a/src/interfaces/wrapperLike.ts +++ b/src/interfaces/wrapperLike.ts @@ -1,14 +1,24 @@ -import { DOMWrapper } from '../domWrapper' +import { + DefinedComponent, + FindAllComponentsSelector, + FindComponentSelector, + NameSelector, + RefSelector +} from 'src/types' +import { VueWrapper } from 'src/vueWrapper' +import { ComponentPublicInstance, FunctionalComponent } from 'vue' +import type { DOMWrapper } from '../domWrapper' export default interface WrapperLike { + readonly element: Node find( selector: K ): DOMWrapper find( selector: K ): DOMWrapper - find(selector: string): DOMWrapper - find(selector: string): DOMWrapper + find(selector: string | RefSelector): DOMWrapper + find(selector: string | RefSelector): DOMWrapper findAll( selector: K @@ -16,9 +26,38 @@ export default interface WrapperLike { findAll( selector: K ): DOMWrapper[] + findAll(selector: string): DOMWrapper[] findAll(selector: string): DOMWrapper[] + findComponent(selector: string): WrapperLike + findComponent( + selector: T | Exclude + ): VueWrapper> + findComponent( + selector: T | string + ): DOMWrapper + findComponent( + selector: NameSelector | RefSelector + ): VueWrapper + findComponent( + selector: T | FindComponentSelector + ): VueWrapper + findComponent(selector: FindComponentSelector): WrapperLike + + findAllComponents(selector: string): WrapperLike[] + findAllComponents( + selector: T | Exclude + ): VueWrapper>[] + findAllComponents( + selector: T | string + ): DOMWrapper[] + findAllComponents(selector: NameSelector): VueWrapper[] + findAllComponents( + selector: T | FindAllComponentsSelector + ): VueWrapper[] + findAllComponents(selector: FindAllComponentsSelector): WrapperLike[] + get( selector: K ): Omit, 'exists'> @@ -28,7 +67,33 @@ export default interface WrapperLike { get(selector: string): Omit, 'exists'> get(selector: string): Omit, 'exists'> + getComponent(selector: string): Omit + getComponent( + selector: T | Exclude + ): Omit>, 'exists'> + // searching for functional component results in DOMWrapper + getComponent( + selector: T | string + ): Omit, 'exists'> + // searching by name or ref always results in VueWrapper + getComponent( + selector: NameSelector | RefSelector + ): Omit + getComponent( + selector: T | FindComponentSelector + ): Omit, 'exists'> + // catch all declaration + getComponent( + selector: FindComponentSelector + ): Omit + html(): string + attributes(): { [key: string]: string } + attributes(key: string): string + attributes(key?: string): { [key: string]: string } | string + + exists(): boolean + setValue(value: any): Promise } diff --git a/src/mount.ts b/src/mount.ts index e82340dee..873a04e87 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -31,7 +31,7 @@ import { mergeGlobalProperties } from './utils' import { processSlot } from './utils/compileSlots' -import { createWrapper, VueWrapper } from './vueWrapper' +import { VueWrapper } from './vueWrapper' import { attachEmitListener } from './emit' import { stubComponents, addToDoNotStubComponents, registerStub } from './stubs' import { @@ -39,6 +39,7 @@ import { unwrapLegacyVueExtendComponent } from './utils/vueCompatSupport' import { trackInstance } from './utils/autoUnmount' +import { createVueWrapper } from './wrapperFactory' // NOTE this should come from `vue` type PublicProps = VNodeProps & AllowedComponentProps & ComponentCustomProps @@ -481,7 +482,7 @@ export function mount( return Reflect.has(appRef, property) } console.warn = warnSave - const wrapper = createWrapper(app, appRef, setProps) + const wrapper = createVueWrapper(app, appRef, setProps) trackInstance(wrapper) return wrapper } diff --git a/src/types.ts b/src/types.ts index 5d0f37d53..9e0b2d094 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,27 +5,24 @@ import { Plugin, AppConfig, VNode, - VNodeProps + VNodeProps, + FunctionalComponent } from 'vue' -interface RefSelector { +export interface RefSelector { ref: string } - -interface NameSelector { - name: string -} - -interface RefSelector { - ref: string -} - -interface NameSelector { +export interface NameSelector { name: string + length?: never } -export type FindComponentSelector = RefSelector | NameSelector | string -export type FindAllComponentsSelector = NameSelector | string +export type FindAllComponentsSelector = + | DefinedComponent + | FunctionalComponent + | NameSelector + | string +export type FindComponentSelector = RefSelector | FindAllComponentsSelector export type Slot = VNode | string | { render: Function } | Function | Component @@ -143,3 +140,5 @@ export type GlobalMountOptions = { } export type VueElement = Element & { __vue_app__?: any } + +export type DefinedComponent = new (...args: any[]) => any diff --git a/src/utils.ts b/src/utils.ts index 614e1666f..3ca2bc8ca 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -93,7 +93,7 @@ export function isObjectComponent( return Boolean(component && typeof component === 'object') } -export function textContent(element: Element): string { +export function textContent(element: Node): string { // we check if the element is a comment first // to return an empty string in that case, instead of the comment content return element.nodeType !== Node.COMMENT_NODE diff --git a/src/utils/find.ts b/src/utils/find.ts index 4fc44fa17..7d04ce5bd 100644 --- a/src/utils/find.ts +++ b/src/utils/find.ts @@ -1,5 +1,5 @@ import { - ComponentPublicInstance, + ComponentInternalInstance, VNode, VNodeArrayChildren, VNodeNormalizedChildren @@ -149,7 +149,7 @@ function findAllVNodes( export function find( root: VNode, selector: FindAllComponentsSelector -): ComponentPublicInstance[] { +): ComponentInternalInstance[] { let matchingVNodes = findAllVNodes(root, selector) if (typeof selector === 'string') { @@ -159,5 +159,5 @@ export function find( ) } - return matchingVNodes.map((vnode: VNode) => vnode.component!.proxy!) + return matchingVNodes.map((vnode: VNode) => vnode.component!) } diff --git a/src/utils/isElement.ts b/src/utils/isElement.ts new file mode 100644 index 000000000..af8e0ed46 --- /dev/null +++ b/src/utils/isElement.ts @@ -0,0 +1,3 @@ +export function isElement(element: Node): element is Element { + return element instanceof Element +} diff --git a/src/vueWrapper.ts b/src/vueWrapper.ts index 6647d15a4..399fae80b 100644 --- a/src/vueWrapper.ts +++ b/src/vueWrapper.ts @@ -1,35 +1,41 @@ -import { ComponentPublicInstance, nextTick, App } from 'vue' +import { + nextTick, + App, + ComponentCustomProperties, + ComponentPublicInstance +} from 'vue' import { ShapeFlags } from '@vue/shared' // @ts-ignore todo - No DefinitelyTyped package exists for this import pretty from 'pretty' import { config } from './config' import domEvents from './constants/dom-events' -import { DOMWrapper } from './domWrapper' -import { - FindAllComponentsSelector, - FindComponentSelector, - VueElement -} from './types' -import { createWrapperError } from './errorWrapper' -import { find, matches } from './utils/find' +import { VueElement } from './types' import { mergeDeep } from './utils' import { emitted, recordEvent } from './emit' import BaseWrapper from './baseWrapper' -import WrapperLike from './interfaces/wrapperLike' - -export class VueWrapper - extends BaseWrapper - implements WrapperLike -{ +import { + createDOMWrapper, + registerFactory, + WrapperType +} from './wrapperFactory' + +export class VueWrapper< + T extends Omit< + ComponentPublicInstance, + '$emit' | keyof ComponentCustomProperties + > & { + $emit: (event: any, ...args: any[]) => void + } & ComponentCustomProperties = ComponentPublicInstance +> extends BaseWrapper { private componentVM: T - private rootVM: ComponentPublicInstance | null + private rootVM: ComponentPublicInstance | undefined | null private __app: App | null private __setProps: ((props: Record) => void) | undefined constructor( app: App | null, - vm: ComponentPublicInstance, + vm: T, setProps?: (props: Record) => void ) { super(vm?.$el) @@ -66,6 +72,10 @@ export class VueWrapper return this.vm.$el.parentElement } + getCurrentComponent() { + return this.vm.$ + } + private attachNativeEventListener(): void { const vm = this.vm if (!vm) return @@ -125,102 +135,8 @@ export class VueWrapper return pretty(this.element.outerHTML) } - find( - selector: K - ): DOMWrapper - find( - selector: K - ): DOMWrapper - find(selector: string): DOMWrapper - find(selector: string): DOMWrapper { - const result = this.parentElement['__vue_app__'] - ? // force using the parentElement to allow finding the root element - this.parentElement.querySelector(selector) - : this.element.querySelector && this.element.querySelector(selector) - - if (result) { - return new DOMWrapper(result, createWrapper) - } - - return createWrapperError('DOMWrapper') - } - - findComponent( - selector: FindComponentSelector | (new () => T) - ): VueWrapper { - if (typeof selector === 'object' && 'ref' in selector) { - const result = this.vm.$refs[selector.ref] - if (result && !(result instanceof HTMLElement)) { - return createWrapper(null, result as T) - } else { - return createWrapperError('VueWrapper') - } - } - - // https://github.com/vuejs/vue-test-utils-next/issues/211 - // VTU v1 supported finding the component mounted itself. - // eg: mount(Comp).findComponent(Comp) - // this is the same as doing `wrapper.vm`, but we keep this behavior for back compat. - if (matches(this.vm.$.vnode, selector)) { - return createWrapper(null, this.vm.$.vnode.component?.proxy!) - } - - const result = find(this.vm.$.subTree, selector) - if (result.length) { - return createWrapper(null, result[0]) - } - - 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[] { - return find(this.vm.$.subTree, selector).map((c) => createWrapper(null, c)) - } - - findAll( - selector: K - ): DOMWrapper[] - findAll( - selector: K - ): DOMWrapper[] - findAll(selector: string): DOMWrapper[] - findAll(selector: string): DOMWrapper[] { - const results = this.parentElement['__vue_app__'] - ? this.parentElement.querySelectorAll(selector) - : this.element.querySelectorAll - ? this.element.querySelectorAll(selector) - : ([] as unknown as NodeListOf) - - return Array.from(results).map( - (element) => new DOMWrapper(element, createWrapper) - ) - } - isVisible(): boolean { - const domWrapper = new DOMWrapper(this.element, createWrapper) + const domWrapper = createDOMWrapper(this.element) return domWrapper.isVisible() } @@ -256,10 +172,7 @@ export class VueWrapper } } -export function createWrapper( - app: App | null, - vm: ComponentPublicInstance, - setProps?: (props: Record) => void -): VueWrapper { - return new VueWrapper(app, vm, setProps) -} +registerFactory( + WrapperType.VueWrapper, + (app, vm, setProps) => new VueWrapper(app, vm, setProps) +) diff --git a/src/wrapperFactory.ts b/src/wrapperFactory.ts new file mode 100644 index 000000000..c9c18893c --- /dev/null +++ b/src/wrapperFactory.ts @@ -0,0 +1,40 @@ +import { ComponentPublicInstance, App } from 'vue' +import type { DOMWrapper as DOMWrapperType } from './domWrapper' +import type { VueWrapper as VueWrapperType } from './vueWrapper' + +export enum WrapperType { + DOMWrapper, + VueWrapper +} + +type DOMWrapperFactory = (element: T) => DOMWrapperType +type VueWrapperFactory = ( + app: App | null, + vm: T, + setProps?: (props: Record) => Promise +) => VueWrapperType + +const factories: { + [WrapperType.DOMWrapper]?: DOMWrapperFactory + [WrapperType.VueWrapper]?: VueWrapperFactory +} = {} + +export function registerFactory( + type: WrapperType.DOMWrapper, + fn: DOMWrapperFactory +): void +export function registerFactory( + type: WrapperType.VueWrapper, + fn: VueWrapperFactory +): void +export function registerFactory( + type: WrapperType.DOMWrapper | WrapperType.VueWrapper, + fn: any +): void { + factories[type] = fn +} + +export const createDOMWrapper: DOMWrapperFactory = (element) => + factories[WrapperType.DOMWrapper]!(element) +export const createVueWrapper: VueWrapperFactory = (app, vm, setProps) => + factories[WrapperType.VueWrapper]!(app, vm, setProps) diff --git a/test-dts/findComponent.d-test.ts b/test-dts/findComponent.d-test.ts new file mode 100644 index 000000000..78d4f66bc --- /dev/null +++ b/test-dts/findComponent.d-test.ts @@ -0,0 +1,83 @@ +import { expectType } from './index' +import { defineComponent } from 'vue' +import { DOMWrapper, mount, VueWrapper } from '../src' +import WrapperLike from '../src/interfaces/wrapperLike' + +const FuncComponent = () => 'hello' + +const ComponentToFind = defineComponent({ + props: { + a: { + type: String, + required: true + } + }, + template: '' +}) + +const ComponentWithEmits = defineComponent({ + emits: { + hi: () => true + }, + props: [], + template: '' +}) + +const AppWithDefine = defineComponent({ + template: '' +}) + +const wrapper = mount(AppWithDefine) + +// find by type - component definition +const componentByType = wrapper.findComponent(ComponentToFind) +expectType>>(componentByType) + +// find by type - component definition with emits +const componentWithEmitsByType = wrapper.findComponent(ComponentWithEmits) +expectType>>( + componentWithEmitsByType +) + +// find by type - functional +const functionalComponentByType = wrapper.findComponent(FuncComponent) +expectType>(functionalComponentByType) + +// find by string +const componentByString = wrapper.findComponent('.foo') +expectType(componentByString) + +// findi by string with specifying component +const componentByStringWithParam = + wrapper.findComponent('.foo') +expectType>>( + componentByStringWithParam +) + +const functionalComponentByStringWithParam = + wrapper.findComponent('.foo') +expectType>(functionalComponentByStringWithParam) + +// find by ref +const componentByRef = wrapper.findComponent({ ref: 'foo' }) +expectType(componentByRef) + +// find by ref with specifying component +const componentByRefWithType = wrapper.findComponent({ + ref: 'foo' +}) +expectType>>( + componentByRefWithType +) + +// find by name +const componentByName = wrapper.findComponent({ name: 'foo' }) +expectType(componentByName) + +// find by name with specifying component +const componentByNameWithType = wrapper.findComponent({ + name: 'foo' +}) +expectType>>( + componentByNameWithType +) diff --git a/test-dts/getComponent.d-test.ts b/test-dts/getComponent.d-test.ts index 5914c9566..ad0e7af9e 100644 --- a/test-dts/getComponent.d-test.ts +++ b/test-dts/getComponent.d-test.ts @@ -1,6 +1,7 @@ import { expectType } from './index' import { defineComponent, ComponentPublicInstance } from 'vue' import { mount } from '../src' +import WrapperLike from '../src/interfaces/wrapperLike' const ComponentToFind = defineComponent({ props: { @@ -30,8 +31,8 @@ expectType(componentByName.vm) // get by string const componentByString = wrapper.getComponent('other') -// returns a wrapper with a generic vm (any) -expectType(componentByString.vm) +// returns a wrapper with WrapperLike (no vm as it could be a functional component) +expectType>(componentByString) // get by ref const componentByRef = wrapper.getComponent({ ref: 'ref' }) diff --git a/tests/features/plugins.spec.ts b/tests/features/plugins.spec.ts index 1f1fa35ba..64b33ec1a 100644 --- a/tests/features/plugins.spec.ts +++ b/tests/features/plugins.spec.ts @@ -3,7 +3,7 @@ import { ComponentPublicInstance } from 'vue' import { mount, config, VueWrapper } from '../../src' declare module '../../src/vueWrapper' { - interface VueWrapper { + interface VueWrapper { width(): number $el: Element myMethod(): void diff --git a/tests/find.spec.ts b/tests/find.spec.ts index f8114491b..493b6625a 100644 --- a/tests/find.spec.ts +++ b/tests/find.spec.ts @@ -15,6 +15,17 @@ describe('find', () => { expect(wrapper.find('#my-span').exists()).toBe(true) }) + it('find DOM element by ref', () => { + const Component = defineComponent({ + render() { + return h('div', {}, [h('span', { ref: 'span', id: 'my-span' })]) + } + }) + const wrapper = mount(Component) + expect(wrapper.find({ ref: 'span' }).exists()).toBe(true) + expect(wrapper.find({ ref: 'span' }).attributes('id')).toBe('my-span') + }) + it('find using multiple root nodes', () => { const Component = defineComponent({ render() { diff --git a/tests/findAllComponents.spec.ts b/tests/findAllComponents.spec.ts index b5d997ef5..ee005a2c4 100644 --- a/tests/findAllComponents.spec.ts +++ b/tests/findAllComponents.spec.ts @@ -1,6 +1,6 @@ import { mount } from '../src' import Hello from './components/Hello.vue' -import { defineComponent } from 'vue' +import { DefineComponent, defineComponent } from 'vue' const compC = defineComponent({ name: 'ComponentC', @@ -66,16 +66,17 @@ describe('findAllComponents', () => { const wrapper = mount(RootComponent) expect(wrapper.findAllComponents('.in-root')).toHaveLength(1) - expect(wrapper.findAllComponents('.in-root')[0].vm.$options.name).toEqual( - 'NestedChild' - ) + expect( + wrapper.findAllComponents('.in-root')[0].vm.$options.name + ).toEqual('NestedChild') expect(wrapper.findAllComponents('.in-child')).toHaveLength(1) // someone might expect DeepNestedChild here, but // we always return TOP component matching DOM element - expect(wrapper.findAllComponents('.in-child')[0].vm.$options.name).toEqual( - 'NestedChild' - ) + expect( + wrapper.findAllComponents('.in-child')[0].vm.$options + .name + ).toEqual('NestedChild') }) }) diff --git a/tests/findComponent.spec.ts b/tests/findComponent.spec.ts index d69747643..1a97ee80d 100644 --- a/tests/findComponent.spec.ts +++ b/tests/findComponent.spec.ts @@ -62,7 +62,7 @@ describe('findComponent', () => { it('finds component by dom selector', () => { const wrapper = mount(compA) // find by DOM selector - expect(wrapper.findComponent('.C').vm).toHaveProperty( + expect(wrapper.findComponent('.C').vm).toHaveProperty( '$options.name', 'ComponentC' ) @@ -70,7 +70,7 @@ describe('findComponent', () => { it('does allows using complicated DOM selector query', () => { const wrapper = mount(compA) - expect(wrapper.findComponent('.B > .C').vm).toHaveProperty( + expect(wrapper.findComponent('.B > .C').vm).toHaveProperty( '$options.name', 'ComponentC' ) @@ -417,7 +417,7 @@ describe('findComponent', () => { cmp.displayName = 'FuncButton' const Comp = defineComponent({ components: { ChildComponent: cmp }, - template: '
Test
' + template: '
' }) const wrapper = mount(Comp) diff --git a/tests/functionalComponents.spec.ts b/tests/functionalComponents.spec.ts index 2b1f6ea9f..f03026ab6 100644 --- a/tests/functionalComponents.spec.ts +++ b/tests/functionalComponents.spec.ts @@ -1,58 +1,78 @@ -import { mount } from '../src' +import { DOMWrapper, mount, VueWrapper } from '../src' import { h, Slots } from 'vue' import Hello from './components/Hello.vue' describe('functionalComponents', () => { - it('mounts a functional component', () => { - const Foo = (props: { msg: string }) => - h('div', { class: 'foo' }, props.msg) + describe('when mounting functional component', () => { + it('mounts without an error', () => { + const Foo = (props: { msg: string }) => + h('div', { class: 'foo' }, props.msg) - const wrapper = mount(Foo, { - props: { - msg: 'foo' - } + const wrapper = mount(Foo, { + props: { + msg: 'foo' + } + }) + + expect(wrapper.html()).toEqual('
foo
') }) - expect(wrapper.html()).toEqual('
foo
') - }) + it('renders the slots of a functional component', () => { + const Foo = (props: Record, { slots }: { slots: Slots }) => + h('div', { class: 'foo' }, slots) - it('renders the slots of a functional component', () => { - const Foo = (props: Record, { slots }: { slots: Slots }) => - h('div', { class: 'foo' }, slots) + const wrapper = mount(Foo, { + slots: { + default: 'just text' + } + }) - const wrapper = mount(Foo, { - slots: { - default: 'just text' - } + expect(wrapper.html()).toEqual('
just text
') }) - expect(wrapper.html()).toEqual('
just text
') - }) + it('asserts classes', () => { + const Foo = () => h('div', { class: 'foo' }) - it('asserts classes', () => { - const Foo = () => h('div', { class: 'foo' }) + const wrapper = mount(Foo, { + attrs: { + class: 'extra_classes' + } + }) - const wrapper = mount(Foo, { - attrs: { - class: 'extra_classes' - } + expect(wrapper.classes()).toContain('extra_classes') + expect(wrapper.classes()).toContain('foo') }) - expect(wrapper.classes()).toContain('extra_classes') - expect(wrapper.classes()).toContain('foo') - }) + it('uses `find`', () => { + const Foo = () => h('div', { class: 'foo' }, h(Hello)) + const wrapper = mount(Foo) - it('uses `find`', () => { - const Foo = () => h('div', { class: 'foo' }, h(Hello)) - const wrapper = mount(Foo) + expect(wrapper.find('#root').exists()).toBe(true) + }) + + it('uses `findComponent`', () => { + const Foo = () => h('div', { class: 'foo' }, h(Hello)) + const wrapper = mount(Foo) - expect(wrapper.find('#root').exists()).toBe(true) + expect(wrapper.findComponent(Hello).exists()).toBe(true) + }) }) - it('uses `findComponent`', () => { - const Foo = () => h('div', { class: 'foo' }, h(Hello)) - const wrapper = mount(Foo) + describe('when retrieving functional component via findComponent', () => { + const FunctionalChild = () => h('div', { class: 'foo' }, 'hello') + const RootComponent = { + render() { + return h(FunctionalChild) + } + } - expect(wrapper.findComponent(Hello).exists()).toBe(true) + let wrapper: VueWrapper + beforeEach(() => { + wrapper = mount(RootComponent) + }) + + it('returns DOMWrapper', () => { + expect(wrapper.findComponent(FunctionalChild)).toBeInstanceOf(DOMWrapper) + }) }) }) diff --git a/tests/getComponent.spec.ts b/tests/getComponent.spec.ts index d9b06e346..536e77d35 100644 --- a/tests/getComponent.spec.ts +++ b/tests/getComponent.spec.ts @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue' +import { DefineComponent, defineComponent } from 'vue' import { mount, RouterLinkStub, shallowMount } from '../src' import Issue425 from './components/Issue425.vue' @@ -69,12 +69,16 @@ describe('getComponent', () => { // https://github.com/vuejs/vue-test-utils-next/issues/425 it('works with router-link and mount', () => { const wrapper = mount(Issue425, options) - expect(wrapper.getComponent('.link').props('to')).toEqual({ name }) + expect(wrapper.getComponent('.link').props('to')).toEqual({ + name + }) }) // https://github.com/vuejs/vue-test-utils-next/issues/425 it('works with router-link and shallowMount', () => { const wrapper = shallowMount(Issue425, options) - expect(wrapper.getComponent('.link').props('to')).toEqual({ name }) + expect(wrapper.getComponent('.link').props('to')).toEqual({ + name + }) }) })