DEV Community

Cover image for Reliable Component Testing with Vitest's Browser Mode and Playwright
Maya Shavin 🌷☕️🏡
Maya Shavin 🌷☕️🏡

Posted on • Originally published at mayashavin.com

Reliable Component Testing with Vitest's Browser Mode and Playwright

Vitest is great for unit testing. But for frontend components that rely on user interactions, browser events, and other visual states, unit testing alone is not enough. We also need to ensure the component looks and behaves as expected in an actual browser. And to simulate the browser environment, Vitest requires packages like JSDOM or HappyDOM, which are not always reliable as the real ones.

An alternative is to use Playwright's Component Testing. However, this solution requires separate setup and run, which can be cumbersome in many cases.

This is where Vitest's browser mode comes in.

Table of Contents

Prequisites

You should have a Vue project set up with Vue Router and Vitest. If you haven't done so, refer to this post to set up the essentisal Vitest testing environment for your Vue project.

Once ready, let's create our testing component SearchBox.

The SearchBox component

Our SearchBox component accepts a search term and syncs it with the URL query params. Its template is as follows:

  <label for="searchbox">Search</label>
  <input v-model="search" 
    placeholder="Search for a pizza" 
    data-testid="search-input" 
    id="searchbox" />
Enter fullscreen mode Exit fullscreen mode

With the script section:

import { useRouter } from "vue-router";
import { useSearch } from "../composables/useSearch";
import { watch } from "vue";

const props = defineProps({
  searchTerm: {
    type: String,
    required: false,
    default: "",
  }
});

const router = useRouter();

const { search } = useSearch({
  defaultSearch: props.searchTerm,
});

watch(search, (value, prevValue) => {
  if (value === prevValue) return;
  router.replace({ query: { search: value } });
}, {
    immediate: true
});
Enter fullscreen mode Exit fullscreen mode

And in the browser, it will look like this:

Search input box component

Next, we will set up the browser mode for Vitest.

Enable Vitest's browser mode with Playwright

In vitest.config.js, we will setup browser mode as below:

// vitest.config.js
/*...*/
defineConfig({
  test: {
    /**... */
    browser: {
      enabled: true,
      name: 'chromium',
      provider: 'playwright',
      providerOptions: {},
    },
  }
})
Enter fullscreen mode Exit fullscreen mode

In which, we configure the following:

  • enabled: enable the browser mode
  • name: the browser to run the tests in (chromium)
  • provider: the test provider for running the browser, such as playwright
  • providerOptions: additional configuration for the test provider.

We also specify which folder (tests\browser) and the file convention to use, avoiding any conflicts with any existing regular unit tests:

// vitest.config.js
/*...*/
defineConfig({
  test: {
    /**... */
    include: 'tests/browser/**/*.{spec,test}.{js,ts}',
  }
})
Enter fullscreen mode Exit fullscreen mode

With that, we are ready to write our first browser test for SearchBox.

Add the first browser test for SearchBox

In the tests/browser folder, we create a new file SearchBox.spec.js with the following code:

/**SearchBox.spec.js */
import { test, expect, describe } from 'vitest';
import SearchBox from "@/components/SearchBox.vue";

describe('SearchBox', () => {
  test('renders search input', async () => {
    /** Test logic here */
  });
});
Enter fullscreen mode Exit fullscreen mode

To render SearchBox, we use render() from vitest-browser-vue, and pass the initial search term as a prop:

/**SearchBox.spec.js */
/**... */
import { render } from 'vitest-browser-vue';

describe('SearchBox', () => {
  test('renders search input', async () => {
    const component = await render(SearchBox, {
      props: {
        searchTerm: "hello",
      },
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Since SearchBox is using router from useRouter() from Vue Router, we need the following router setup:

  • Create a mock router using createRouter():
  /** SearchBox.spec.js */
  /**... */
  import { routes } from '@/router';
  import { createRouter, createWebHistory } from 'vue-router';

  const router = createRouter({
    history: createWebHistory(),
    routes: routes,
  })
Enter fullscreen mode Exit fullscreen mode
  • Pass it as a global plugin to render():
  /** SearchBox.spec.js */
    test('renders search input', async () => {
      const component = await render(SearchBox, {
        /**... */, 
        global: {
          plugins: [router]
        }
      });
    });
Enter fullscreen mode Exit fullscreen mode

Once done, we locate the input element by its data-testid, and assert its initial value using toHaveValue():

  test('renders search input', async () => {
    /**... */
    const input = await component.getByTestId('search-input')

    await expect(input.element()).toHaveValue('hello')
  });
Enter fullscreen mode Exit fullscreen mode

Note here input received is just a Locator and not a valid HTML element. We need input.element() to get the HTML instance. Otherwise, Vitest will throw the below error:

Error of value needed to be HTML or SVG element

To change the input's value, we use input.fill():

  test('renders search input', async () => {
    /**... */
    await input.fill('test')
  });
Enter fullscreen mode Exit fullscreen mode

Alternatively, we can use userEvent() from @vitest/browser/context as follows:

import { userEvent } from "@vitest/browser/context"

/**... */
  test('renders search input', async () => {
    /**... */    
    await userEvent.fill(input, 'test')
  });
Enter fullscreen mode Exit fullscreen mode

Both approaches perform the same. We can then assert the new value as usual:

await expect(input.element()).toHaveValue('test')
Enter fullscreen mode Exit fullscreen mode

That's it! We have successfully written our first browser test.

At this point, we have one test configuration set for our Vitest runner. This setup will be problematic when Vitest need to run both unit and browser tests together in an automation workflow. For such cases, we use workspace and separate the settings per test type, which we explore next.

Using the workspace configuration file

We create a new file vitest.workspace.js to store the workspace configurations as follows:

import { defineWorkspace } from 'vitest/config'

export default defineWorkspace([
  {
    extends: 'vitest.config.js',
    test: {
      environment: 'jsdom',
      name: 'unit',
      include: ['**/*/unit/*.{spec,test}.{js,ts}'],
    },
  },
])
Enter fullscreen mode Exit fullscreen mode

In which, we define the first configuration for unit tests using jsdom, based on the existing vitest.config.js settings. We also specify the folder and file convention for the unit tests.

Similarly, we define the second configuration for browser tests using playwright:

export default defineWorkspace([
  /** ... */,
  {
    extends: 'vitest.config.js',
    test: {
      include: ['**/*/browser/*.{spec,test}.{js,ts}'],
      browser: {
        enabled: true,
        name: 'chromium',
        provider: 'playwright',
      },
      name: 'browser'
    },
  },
])
Enter fullscreen mode Exit fullscreen mode

And with that, we can run all our tests in a single command, which we will see next.

Run and view the results

We add the following command to our package.json:

{
  "scripts": {
    "test": "vitest --workspace=vitest.workspace.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

Upon executing yarn test, Vitest runs the tests based on vitest.workspace.js and displays the results in a GUI dashboard as follows:

Dashboard of Vitest result with each test labeled by type

Vitest labels each test by unit or browser status. We can then filter the tests by their statuses, or perform further debugging with the given browser UI per test suite.


Summary

We have learned how to set up browser mode for Vitest using Playwright, and write the first browser test. We have also explored how to take screenshots for visual testing, and use the workspace configuration to separate the settings per testing mode. One big limitation of Vitest's browser mode in comparison to Playwright's Component Testing is the lack of browser's address bar, limiting us from testing the component's state synchronization with URL query params in the browser. But it's a good start to build a scalable testing strategy for our Vue projects.

👉 Learn about Vue 3 and TypeScript with my new book Learning Vue!

👉 Follow me on X | LinkedIn.

Like this post or find it helpful? Share it 👇🏼 or Buy me a coffee

Top comments (2)

Collapse
 
alexanderop profile image
Alexander Opalic

I often struggle with test strategy decisions. Should I use Vitest and JSDOM for all tests? This would include using the testing library and mocking the entire Vue app for each test. It's much faster than using Playwright with a real browser. But it has some downsides. What's your take on this for a project? How would you balance tests between Vitest and Playwright with an actual browser?

Collapse
 
mayashavin profile image
Maya Shavin 🌷☕️🏡

This is also often my struggle when it comes to testing :), despite how many projects I built. For me, I tend to:

  • Break the logic into testable code (like utilities, composable) and test them without Vue context if possible.
  • Never write a test just to test the component toBeInDocument() only.
  • Not use toMatchSnapshot(), instead favoring Playwright's screenshot() for actual visual regression tests.
  • Depending on the component's nature, such as an isolated component with a minimum of dependency, Vitest + JSDOM + Vue test utils can be good enough. But for component that requires interaction with or controlled by other component, or browser APIs (such as a Cart component whose state is changeable by an Item's action, or a search box whose query state is synchronised with the browser's URL), there is not much point of unit-testing it with Vitest + JSDOM when you still need to do the same in E2E test :).
  • If a component requires accessibility test, especially behavior test - straight to Playwright + Axecore. No point of checking ARIA attributes in unit test when you can have Axe-core do that for you in the page/view level :).

But really, it all depends on the project's type and what you want to test. If Vitest's browser mode is fully production-ready, I can say we probably won't need JSDOM anymore for independent component testing.

Btw, I did a talk on similar topic on choosing testing strategy when it comes to component testing with Vue Nation a while ago. You can check the video out, or take a look at my slides.

Hope it helps :)