Skip to content

Commit

Permalink
docs: how to test teleport (#670)
Browse files Browse the repository at this point in the history
* wip: testing teleport

* lint

* add example with getComponent

* tests around emits and props

* finish article

* improve style

* lint
  • Loading branch information
lmiller1990 authored Jun 23, 2021
1 parent a3e6993 commit 15cd340
Show file tree
Hide file tree
Showing 10 changed files with 444 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const config = {
},
{ text: 'Testing Vuex', link: '/guide/advanced/vuex' },
{ text: 'Testing Vue Router', link: '/guide/advanced/vue-router' },
{ text: 'Testing Teleport', link: '/guide/advanced/teleport' },
{
text: 'Third-party integration',
link: '/guide/advanced/third-party'
Expand Down
189 changes: 189 additions & 0 deletions docs/guide/advanced/teleport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Testing Teleport

Vue 3 comes with a new built-in component: `<Teleport>`, which allows components to "teleport" their content far outside of their own `<template>`. Most tests written with Vue Test Utils are scoped to the component passed to `mount`, which introduces some complexity when it comes to testing a component that is teleported outside of the component where it is initially rendered.

Here are some strategies and techniques for testing components using `<Teleport>`.

## Example

In this example we are testing a `<Navbar>` component. It renders a `<Sigup>` component inside of a `<Teleport>`. The `target` prop of `<Teleport>` is an element located outside of the `<Navbar>` component.

This is the `Navbar.vue` component:

```vue
<template>
<Teleport to="#modal">
<Signup />
</Teleport>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Signup from './Signup.vue'
export default defineComponent({
components: {
Signup
}
})
</script>
```

It simply teleports a `<Signup>` somewhere else. It's simple for the purpose of this example.

`Signup.vue` is a form that validates if `username` is greater than 8 characters. If it is, when submitted, it emits a `signup` event with the `username` as the payload. Testing that will be our goal.

```vue
<template>
<div>
<form @submit.prevent="submit">
<input v-model="username" />
</form>
</div>
</template>
<script>
export default {
emits: ['signup'],
data() {
return {
username: ''
}
},
computed: {
error() {
return this.username.length < 8
}
},
methods: {
submit() {
if (!this.error) {
this.$emit('signup', this.username)
}
}
}
}
</script>
```

## Mounting the Component

Starting with a minimal test:

```ts
import { mount } from '@vue/test-utils'
import Navbar from './Navbar.vue'
import Signup from './Signup.vue'

test('emits a signup event when valid', async () => {
const wrapper = mount(Navbar)
})
```

Running this test will give you a warning: `[Vue warn]: Failed to locate Teleport target with selector "#modal"`. Let's create it:

```ts {5-15}
import { mount } from '@vue/test-utils'
import Navbar from './Navbar.vue'
import Signup from './Signup.vue'

beforeEach(() => {
// create teleport target
const el = document.createElement('div')
el.id = 'modal'
document.body.appendChild(el)
})

afterEach(() => {
// clean up
document.body.outerHTML = ''
})

test('teleport', async () => {
const wrapper = mount(Navbar)
})
```

We are using Jest for this example, which does not reset the DOM every test. For this reason, it's good to clean up after each test with `afterEach`.

## Interacting with the Teleported Component

The next thing to do is fill out the username input. Unfortunately, we cannot use `wrapper.find('input')`. Why not? A quick `console.log(wrapper.html())` shows us:

```html
<!--teleport start-->
<!--teleport end-->
```

We see some comments used by Vue to handle `<Teleport>` - but no `<input>`. That's because the `<Signup>` component (and its HTML) are not rendered inside of `<Navbar>` anymore - it was teleported outside.

Although the actual HTML is teleported outside, it turns out the Virtual DOM associated with `<Navbar>` maintains a reference to the original component. This means you can use `getComponent` and `findComponent, which operate on the Virtual DOM, not the regular DOM.

```ts {12}
beforeEach(() => {
// ...
})

afterEach(() => {
// ...
})

test('teleport', async () => {
const wrapper = mount(Navbar)

wrapper.getComponent(Signup) // got it!
})
```

`getComponent` returns a `VueWrapper`. Now you can use methods like `get`, `find` and `trigger`.

Let's finish the test:

```ts {4-8}
test('teleport', async () => {
const wrapper = mount(Navbar)

const signup = wrapper.getComponent(Signup)
await signup.get('input').setValue('valid_username')
await signup.get('form').trigger('submit.prevent')

expect(signup.emitted().signup[0]).toEqual(['valid_username'])
})
```

It passes!

The full test:

```ts
import { mount } from '@vue/test-utils'
import Navbar from './Navbar.vue'
import Signup from './Signup.vue'

beforeEach(() => {
// create teleport target
const el = document.createElement('div')
el.id = 'modal'
document.body.appendChild(el)
})

afterEach(() => {
// clean up
document.body.outerHTML = ''
})

test('teleport', async () => {
const wrapper = mount(Navbar)

const signup = wrapper.getComponent(Signup)
await signup.get('input').setValue('valid_username')
await signup.get('form').trigger('submit.prevent')

expect(signup.emitted().signup[0]).toEqual(['valid_username'])
})
```

## Conclusion

- Create a teleport target with `document.createElement`.
- Find teleported components using `getComponent` or `findComponent` which operate on the Virtual DOM level.
18 changes: 18 additions & 0 deletions tests/components/EmitsEvent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<template>
<button @click="greet" />
</template>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
emits: ['greet'],
setup(props, { emit }) {
return {
greet: () => {
emit('greet', 'Hey!')
}
}
}
})
</script>
9 changes: 9 additions & 0 deletions tests/components/WithTeleportEmitsComp.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<teleport to="#somewhere">
<emits-event msg="hi there" />
</teleport>
</template>

<script setup lang="ts">
import EmitsEvent from './EmitsEvent.vue'
</script>
9 changes: 9 additions & 0 deletions tests/components/WithTeleportPropsComp.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<teleport to="#somewhere">
<with-props msg="hi there" />
</teleport>
</template>

<script setup lang="ts">
import WithProps from './WithProps.vue'
</script>
16 changes: 16 additions & 0 deletions tests/docs-examples/Navbar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<template>
<Teleport to="#modal">
<Signup />
</Teleport>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import Signup from './Signup.vue'
export default defineComponent({
components: {
Signup
}
})
</script>
30 changes: 30 additions & 0 deletions tests/docs-examples/Signup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<div>
<form @submit.prevent="submit">
<input v-model="username" />
</form>
</div>
</template>

<script>
export default {
emits: ['signup'],
data() {
return {
username: ''
}
},
computed: {
error() {
return this.username.length < 8
}
},
methods: {
submit() {
if (!this.error) {
this.$emit('signup', this.username)
}
}
}
}
</script>
25 changes: 25 additions & 0 deletions tests/docs-examples/teleport.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { mount } from '../../src'
import Navbar from './Navbar.vue'
import Signup from './Signup.vue'

beforeEach(() => {
// create teleport target
const el = document.createElement('div')
el.id = 'modal'
document.body.appendChild(el)
})

afterEach(() => {
// clean up
document.body.outerHTML = ''
})

test('teleport', async () => {
const wrapper = mount(Navbar)

const signup = wrapper.getComponent(Signup)
await signup.get('input').setValue('valid_username')
await signup.get('form').trigger('submit.prevent')

expect(signup.emitted().signup[0]).toEqual(['valid_username'])
})
Loading

0 comments on commit 15cd340

Please sign in to comment.