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
- Table of Contents
- Prequisites
- The SearchBox component
- Enable Vitest's browser mode with Playwright
- Add the first browser test for SearchBox
- Using the workspace configuration file
- Run and view the results
- Summary
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" />
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
});
And in the browser, it will look like this:
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: {},
},
}
})
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 asplaywright
-
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}',
}
})
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 */
});
});
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",
},
});
});
});
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,
})
- Pass it as a global plugin to
render()
:
/** SearchBox.spec.js */
test('renders search input', async () => {
const component = await render(SearchBox, {
/**... */,
global: {
plugins: [router]
}
});
});
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')
});
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:
To change the input's value, we use input.fill()
:
test('renders search input', async () => {
/**... */
await input.fill('test')
});
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')
});
Both approaches perform the same. We can then assert the new value as usual:
await expect(input.element()).toHaveValue('test')
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}'],
},
},
])
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'
},
},
])
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"
}
}
Upon executing yarn test
, Vitest runs the tests based on vitest.workspace.js
and displays the results in a GUI dashboard as follows:
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)
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?
This is also often my struggle when it comes to testing :), despite how many projects I built. For me, I tend to:
toBeInDocument()
only.toMatchSnapshot()
, instead favoring Playwright'sscreenshot()
for actual visual regression tests.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 :)