Skip to content

Commit 15cd340

Browse files
authored
docs: how to test teleport (#670)
* wip: testing teleport * lint * add example with getComponent * tests around emits and props * finish article * improve style * lint
1 parent a3e6993 commit 15cd340

File tree

10 files changed

+444
-0
lines changed

10 files changed

+444
-0
lines changed

docs/.vitepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const config = {
8888
},
8989
{ text: 'Testing Vuex', link: '/guide/advanced/vuex' },
9090
{ text: 'Testing Vue Router', link: '/guide/advanced/vue-router' },
91+
{ text: 'Testing Teleport', link: '/guide/advanced/teleport' },
9192
{
9293
text: 'Third-party integration',
9394
link: '/guide/advanced/third-party'

docs/guide/advanced/teleport.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Testing Teleport
2+
3+
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.
4+
5+
Here are some strategies and techniques for testing components using `<Teleport>`.
6+
7+
## Example
8+
9+
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.
10+
11+
This is the `Navbar.vue` component:
12+
13+
```vue
14+
<template>
15+
<Teleport to="#modal">
16+
<Signup />
17+
</Teleport>
18+
</template>
19+
20+
<script lang="ts">
21+
import { defineComponent } from 'vue'
22+
import Signup from './Signup.vue'
23+
24+
export default defineComponent({
25+
components: {
26+
Signup
27+
}
28+
})
29+
</script>
30+
```
31+
32+
It simply teleports a `<Signup>` somewhere else. It's simple for the purpose of this example.
33+
34+
`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.
35+
36+
```vue
37+
<template>
38+
<div>
39+
<form @submit.prevent="submit">
40+
<input v-model="username" />
41+
</form>
42+
</div>
43+
</template>
44+
45+
<script>
46+
export default {
47+
emits: ['signup'],
48+
data() {
49+
return {
50+
username: ''
51+
}
52+
},
53+
computed: {
54+
error() {
55+
return this.username.length < 8
56+
}
57+
},
58+
methods: {
59+
submit() {
60+
if (!this.error) {
61+
this.$emit('signup', this.username)
62+
}
63+
}
64+
}
65+
}
66+
</script>
67+
```
68+
69+
## Mounting the Component
70+
71+
Starting with a minimal test:
72+
73+
```ts
74+
import { mount } from '@vue/test-utils'
75+
import Navbar from './Navbar.vue'
76+
import Signup from './Signup.vue'
77+
78+
test('emits a signup event when valid', async () => {
79+
const wrapper = mount(Navbar)
80+
})
81+
```
82+
83+
Running this test will give you a warning: `[Vue warn]: Failed to locate Teleport target with selector "#modal"`. Let's create it:
84+
85+
```ts {5-15}
86+
import { mount } from '@vue/test-utils'
87+
import Navbar from './Navbar.vue'
88+
import Signup from './Signup.vue'
89+
90+
beforeEach(() => {
91+
// create teleport target
92+
const el = document.createElement('div')
93+
el.id = 'modal'
94+
document.body.appendChild(el)
95+
})
96+
97+
afterEach(() => {
98+
// clean up
99+
document.body.outerHTML = ''
100+
})
101+
102+
test('teleport', async () => {
103+
const wrapper = mount(Navbar)
104+
})
105+
```
106+
107+
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`.
108+
109+
## Interacting with the Teleported Component
110+
111+
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:
112+
113+
```html
114+
<!--teleport start-->
115+
<!--teleport end-->
116+
```
117+
118+
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.
119+
120+
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.
121+
122+
```ts {12}
123+
beforeEach(() => {
124+
// ...
125+
})
126+
127+
afterEach(() => {
128+
// ...
129+
})
130+
131+
test('teleport', async () => {
132+
const wrapper = mount(Navbar)
133+
134+
wrapper.getComponent(Signup) // got it!
135+
})
136+
```
137+
138+
`getComponent` returns a `VueWrapper`. Now you can use methods like `get`, `find` and `trigger`.
139+
140+
Let's finish the test:
141+
142+
```ts {4-8}
143+
test('teleport', async () => {
144+
const wrapper = mount(Navbar)
145+
146+
const signup = wrapper.getComponent(Signup)
147+
await signup.get('input').setValue('valid_username')
148+
await signup.get('form').trigger('submit.prevent')
149+
150+
expect(signup.emitted().signup[0]).toEqual(['valid_username'])
151+
})
152+
```
153+
154+
It passes!
155+
156+
The full test:
157+
158+
```ts
159+
import { mount } from '@vue/test-utils'
160+
import Navbar from './Navbar.vue'
161+
import Signup from './Signup.vue'
162+
163+
beforeEach(() => {
164+
// create teleport target
165+
const el = document.createElement('div')
166+
el.id = 'modal'
167+
document.body.appendChild(el)
168+
})
169+
170+
afterEach(() => {
171+
// clean up
172+
document.body.outerHTML = ''
173+
})
174+
175+
test('teleport', async () => {
176+
const wrapper = mount(Navbar)
177+
178+
const signup = wrapper.getComponent(Signup)
179+
await signup.get('input').setValue('valid_username')
180+
await signup.get('form').trigger('submit.prevent')
181+
182+
expect(signup.emitted().signup[0]).toEqual(['valid_username'])
183+
})
184+
```
185+
186+
## Conclusion
187+
188+
- Create a teleport target with `document.createElement`.
189+
- Find teleported components using `getComponent` or `findComponent` which operate on the Virtual DOM level.

tests/components/EmitsEvent.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<template>
2+
<button @click="greet" />
3+
</template>
4+
5+
<script lang="ts">
6+
import { defineComponent } from 'vue'
7+
8+
export default defineComponent({
9+
emits: ['greet'],
10+
setup(props, { emit }) {
11+
return {
12+
greet: () => {
13+
emit('greet', 'Hey!')
14+
}
15+
}
16+
}
17+
})
18+
</script>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<template>
2+
<teleport to="#somewhere">
3+
<emits-event msg="hi there" />
4+
</teleport>
5+
</template>
6+
7+
<script setup lang="ts">
8+
import EmitsEvent from './EmitsEvent.vue'
9+
</script>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<template>
2+
<teleport to="#somewhere">
3+
<with-props msg="hi there" />
4+
</teleport>
5+
</template>
6+
7+
<script setup lang="ts">
8+
import WithProps from './WithProps.vue'
9+
</script>

tests/docs-examples/Navbar.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template>
2+
<Teleport to="#modal">
3+
<Signup />
4+
</Teleport>
5+
</template>
6+
7+
<script lang="ts">
8+
import { defineComponent } from 'vue'
9+
import Signup from './Signup.vue'
10+
11+
export default defineComponent({
12+
components: {
13+
Signup
14+
}
15+
})
16+
</script>

tests/docs-examples/Signup.vue

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<template>
2+
<div>
3+
<form @submit.prevent="submit">
4+
<input v-model="username" />
5+
</form>
6+
</div>
7+
</template>
8+
9+
<script>
10+
export default {
11+
emits: ['signup'],
12+
data() {
13+
return {
14+
username: ''
15+
}
16+
},
17+
computed: {
18+
error() {
19+
return this.username.length < 8
20+
}
21+
},
22+
methods: {
23+
submit() {
24+
if (!this.error) {
25+
this.$emit('signup', this.username)
26+
}
27+
}
28+
}
29+
}
30+
</script>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { mount } from '../../src'
2+
import Navbar from './Navbar.vue'
3+
import Signup from './Signup.vue'
4+
5+
beforeEach(() => {
6+
// create teleport target
7+
const el = document.createElement('div')
8+
el.id = 'modal'
9+
document.body.appendChild(el)
10+
})
11+
12+
afterEach(() => {
13+
// clean up
14+
document.body.outerHTML = ''
15+
})
16+
17+
test('teleport', async () => {
18+
const wrapper = mount(Navbar)
19+
20+
const signup = wrapper.getComponent(Signup)
21+
await signup.get('input').setValue('valid_username')
22+
await signup.get('form').trigger('submit.prevent')
23+
24+
expect(signup.emitted().signup[0]).toEqual(['valid_username'])
25+
})

0 commit comments

Comments
 (0)