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
-
- My Global Component
-
+ My Global Component
```
@@ -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
+ })
})
})