How to use and Test Vue Template Refs with Vitest

When working with Vue we sometimes need access to DOM elements or child components to manipulate them manually instead of relying on data binding. Luckily, we can use Template Refs to do just that.

Some use cases for template refs would be to:

  • Automatically focus an input when the page loads.
  • Manipulate the value of the input programmatically.
  • Mount a JS library that requires a DOM element, like ChartJS.

In this tutorial, let’s explore the simple use case of auto-focusing an input. Afterward, we will develop unit tests for it and tackle the edge cases that arise from using refs using Vitest.

Accessing DOM elements

First, create a Search.vue component:

<script setup lang="ts">
  import { onMounted, ref } from 'vue'

  const model = defineModel<string>()
</script>

<template>
  <div>
    <input
      v-model="model"
    />
  </div>
</template>

The component has a single element, an input, and a model that will expose the search query to the parent.

Next, add the template ref:

<script setup lang="ts">
  import { onMounted, ref } from 'vue'

  const input = ref<HTMLInputElement | null>(null)
  const model = defineModel<string>()
</script>

<template>
  <div>
    <input
      ref="input"
      v-model="model"
    />
  </div>
</template>

Finally, to focus on the input, access the refs value and call the focus method:

<script setup lang="ts">
  import { onMounted, ref } from 'vue'

  const input = ref<HTMLInputElement | null>(null)
  const model = defineModel<string>()

  onMounted(() => {
    input.value?.focus()
  })
</script>

<template>
  <div>
    <input
      ref="input"
      v-model="model"
    />
  </div>
</template>

Very simple, right? Remember that the input ref will only be defined once the component is mounted.

Testing Template Refs

Now that we have a working example, let’s explore testing it. We will test that when the component is mounted, it automatically focuses on the input.

Finding out if the input ref is focused or not can be a bit tricky. A prerequisite is to have an environment specified. This can be jsdom or happy-dom.

Adjust the vite config to use either:

/// <reference types="vitest" />
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
  },
})

This way, vue test utils can mount the component onto a fake document that we can reference in our tests. You will see how this is useful in the next bit.

Next, write the unit test:

import { describe, expect, test } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import Input from './Input.vue'

describe('Search', () => {
  describe('when mounted', () => {
    test('focuses on the input', () => {
      const wrapper = shallowMount(Input, {
        attachTo: document.body,
      })
      const input = wrapper.find<HTMLInputElement>({
        ref: 'input',
      })

      expect(document.activeElement).toBe(input.element)
    })
  })
})

And when running the test, it should work:

vue template refs test passing

There are a few things happening here. First, we use attachTo so that the component is rendered and added to our fake document (remember why we added jsdom earlier?)

Second, we use document.activeElement which holds a reference to the focused element and checks if it’s equal to the input element we have in our component. that way we can assert if focusing is working as expected.

As a side note, using .toBe to compare HTML elements is safe, but you can also do:

expect(document.activeElement?.isSameNode(input.element)).toBe(true)
Amenallah Hsoumi
Amenallah Hsoumi

Senior Software Engineer, Indie Hacker, building cool stuff on the web!

Articles: 19