Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 5 additions & 35 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -374,9 +375,7 @@ export default {

```vue
<template>
<div class="global-component">
My Global Component
</div>
<div class="global-component">My Global Component</div>
</template>
```

Expand Down Expand Up @@ -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]
})
```

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
:::
Expand Down Expand Up @@ -1862,8 +1832,8 @@ function shallowMount(Component, options?: MountingOptions): VueWrapper

## enableAutoUnmount


**Signature:**

```ts
enableAutoUnmount(hook: Function));
disableAutoUnmount(): void;
Expand Down
215 changes: 195 additions & 20 deletions src/baseWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ElementType extends Element> {
export default abstract class BaseWrapper<ElementType extends Node>
implements WrapperLike
{
private readonly wrapperElement: ElementType & {
__vueParentComponent?: ComponentInternalInstance
}
Expand All @@ -24,33 +39,170 @@ export default abstract 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)
find<K extends keyof HTMLElementTagNameMap>(
selector: K
): DOMWrapper<HTMLElementTagNameMap[K]>
find<K extends keyof SVGElementTagNameMap>(
selector: K
): DOMWrapper<SVGElementTagNameMap[K]>
find<T extends Element>(selector: string | RefSelector): DOMWrapper<T>
find(selector: string | RefSelector): DOMWrapper<Element>
find(selector: string | RefSelector): DOMWrapper<Element> {
// 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<K extends keyof HTMLElementTagNameMap>(
selector: K
): DOMWrapper<HTMLElementTagNameMap[K]>[]
findAll<K extends keyof SVGElementTagNameMap>(
selector: K
): DOMWrapper<SVGElementTagNameMap[K]>[]
findAll<T extends Element>(selector: string): DOMWrapper<T>[]
findAll(selector: string): DOMWrapper<Element>[] {
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<T extends never>(selector: string): WrapperLike
// searching for component created via defineComponent results in VueWrapper of proper type
findComponent<T extends DefinedComponent>(
selector: T | Exclude<FindComponentSelector, FunctionalComponent>
): VueWrapper<InstanceType<T>>
// searching for functional component results in DOMWrapper
findComponent<T extends FunctionalComponent>(
selector: T | string
): DOMWrapper<Element>
// searching by name or ref always results in VueWrapper
findComponent<T extends never>(
selector: NameSelector | RefSelector
): VueWrapper
findComponent<T extends ComponentPublicInstance>(
selector: T | FindComponentSelector
): VueWrapper<T>
abstract findAllComponents(
// catch all declaration
findComponent<T extends never>(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<T extends never>(selector: string): WrapperLike[]
findAllComponents<T extends DefinedComponent>(
selector: T | Exclude<FindAllComponentsSelector, FunctionalComponent>
): VueWrapper<InstanceType<T>>[]
findAllComponents<T extends FunctionalComponent>(
selector: T | string
): DOMWrapper<Element>[]
findAllComponents<T extends never>(selector: NameSelector): VueWrapper[]
findAllComponents<T extends ComponentPublicInstance>(
selector: T | FindAllComponentsSelector
): VueWrapper<T>[]
// catch all declaration
findAllComponents<T extends never>(
selector: FindAllComponentsSelector
): VueWrapper<any>[]
): 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<void>
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<string, string> = {}
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
Expand Down Expand Up @@ -80,13 +232,30 @@ export default abstract class BaseWrapper<ElementType extends Element> {
throw new Error(`Unable to get ${selector} within: ${this.html()}`)
}

getComponent<T extends never>(selector: string): Omit<WrapperLike, 'exists'>
getComponent<T extends DefinedComponent>(
selector: T | Exclude<FindComponentSelector, FunctionalComponent>
): Omit<VueWrapper<InstanceType<T>>, 'exists'>
// searching for functional component results in DOMWrapper
getComponent<T extends FunctionalComponent>(
selector: T | string
): Omit<DOMWrapper<Element>, 'exists'>
// searching by name or ref always results in VueWrapper
getComponent<T extends never>(
selector: NameSelector | RefSelector
): Omit<VueWrapper, 'exists'>
getComponent<T extends ComponentPublicInstance>(
selector: FindComponentSelector | (new () => T)
): Omit<VueWrapper<T>, 'exists'> {
selector: T | FindComponentSelector
): Omit<VueWrapper<T>, 'exists'>
// catch all declaration
getComponent<T extends never>(
selector: FindComponentSelector
): Omit<WrapperLike, 'exists'>
getComponent(selector: FindComponentSelector): Omit<WrapperLike, 'exists'> {
const result = this.findComponent(selector)

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

let message = 'Unable to get '
Expand Down Expand Up @@ -116,13 +285,19 @@ export default abstract class BaseWrapper<ElementType extends Element> {
'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
Expand Down
5 changes: 2 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { ComponentPublicInstance } from 'vue'
import { GlobalMountOptions } from './types'
import { VueWrapper } from './vueWrapper'
import { DOMWrapper } from './domWrapper'

export interface GlobalConfigOptions {
global: Required<GlobalMountOptions>
plugins: {
VueWrapper: Pluggable<VueWrapper<ComponentPublicInstance>>
DOMWrapper: Pluggable<DOMWrapper<Element>>
VueWrapper: Pluggable<VueWrapper>
DOMWrapper: Pluggable<DOMWrapper<Node>>
}
renderStubDefaultSlot: boolean
}
Expand Down
Loading