Unit testing

Learn how to write unit tests for your Nuxt integration.

The Nuxt SDK supports testing with Vitest and Nuxt Test Utils.

Here is what you need to check:

  • Make sure your tracking calls are triggered correctly
  • Verify you have defined fallbacks for your content
  • Ensure you are handling evaluation errors

In the following sections, we will go through each of these points in detail.

Set up Vitest

Install the required dependencies:

npm install -D vitest @nuxt/test-utils @vue/test-utils happy-dom

Create a vitest.config.ts in the root of your project:

12345678910111213
import {defineVitestConfig} from '@nuxt/test-utils/config';
export default defineVitestConfig({  test: {    globals: true,    environment: 'nuxt',    environmentOptions: {      nuxt: {        domEnvironment: 'happy-dom',      },    },  },});

Write tests

The Nuxt SDK fetches content through internal API routes. In tests, you can mock these routes and mount async components using the helpers from Nuxt Test Utils.

Content rendering

Consider a HomeHero component that fetches slot content with a fallback:

components/HomeHero.vue
123456789101112131415161718
<script setup>const {data, error} = await useContent('home-hero', {  fallback: {    title: 'Default title',    subtitle: 'Default subtitle',  },});</script>
<template>  <div v-if="data">    <h1>{{ data.content.title }}</h1>    <p>{{ data.content.subtitle }}</p>  </div>  <div v-else-if="error">    <p>Something went wrong</p>  </div></template>

You can test both the success and error paths by mocking the content endpoint:

test/HomeHero.test.ts
1234567891011121314151617181920212223242526272829303132333435363738
import {describe, it, expect} from 'vitest';import {mountSuspended, registerEndpoint} from '@nuxt/test-utils/runtime';import HomeHero from '../components/HomeHero.vue';
describe('HomeHero', () => {  it('should render the personalized content', async () => {    registerEndpoint('/api/_croct/content', {      method: 'POST',      handler: () => ({        content: {          title: 'Personalized title',          subtitle: 'Personalized subtitle',        },      }),    });
    const wrapper = await mountSuspended(HomeHero);
    expect(wrapper.find('h1').text()).toBe('Personalized title');    expect(wrapper.find('p').text()).toBe('Personalized subtitle');  });
  it('should render the error state on failure', async () => {    registerEndpoint('/api/_croct/content', {      method: 'POST',      handler: () => {        throw createError({          statusCode: 500,          statusMessage: 'Server error',        });      },    });
    const wrapper = await mountSuspended(HomeHero);
    expect(wrapper.text()).toContain('Something went wrong');  });});

Query evaluation

The same approach works for query evaluation. Consider a Greeting component:

components/Greeting.vue
12345678910
<script setup>const {data: returning} = await useEvaluation("user is returning", {  fallback: false,});</script>
<template>  <h1 v-if="returning">Welcome back!</h1>  <h1 v-else>Welcome!</h1></template>

Mock the evaluation endpoint to control the query result:

test/Greeting.test.ts
1234567891011121314151617181920212223242526272829303132
import {describe, it, expect} from 'vitest';import {mountSuspended, registerEndpoint} from '@nuxt/test-utils/runtime';import Greeting from '../components/Greeting.vue';
describe('Greeting', () => {  it('should greet returning users', async () => {    registerEndpoint('/api/_croct/evaluate', {      method: 'POST',      handler: () => true,    });
    const wrapper = await mountSuspended(Greeting);
    expect(wrapper.find('h1').text()).toBe('Welcome back!');  });
  it('should use the fallback on failure', async () => {    registerEndpoint('/api/_croct/evaluate', {      method: 'POST',      handler: () => {        throw createError({          statusCode: 500,          statusMessage: 'Server error',        });      },    });
    const wrapper = await mountSuspended(Greeting);
    expect(wrapper.find('h1').text()).toBe('Welcome!');  });});

Event tracking

Tracking is a client-side feature, so these tests use Vue Test Utils with the createCroct plugin from @croct/plug-nuxt/csr. This gives you direct access to the event listener API.

Croct's mascot amazed
Automatic test mode detection

If you are using Vitest or any other test framework that sets NODE_ENV=test, the SDK auto-detects the test environment and uses a fake transport layer. You do not need to mock the network requests.

Consider a TrackButton component:

components/TrackButton.vue
123456789101112131415
<script setup>import {useCroct} from '@croct/plug-nuxt/csr';
const croct = useCroct();
function onClick() {  croct.track('goalCompleted', {    goalId: 'ctaClicked',  });}</script>
<template>  <button @click="onClick">Click me</button></template>

Mount the component with the Croct plugin and assert on the tracked events:

test/TrackButton.test.ts
123456789101112131415161718192021222324252627282930313233343536373839
import {describe, it, expect, afterEach} from 'vitest';import {mount} from '@vue/test-utils';import {createCroct} from '@croct/plug-nuxt/csr';import croct from '@croct/plug';import TrackButton from '../components/TrackButton.vue';
describe('TrackButton', () => {  afterEach(async () => {    await croct.unplug();  });
  it('should track a goal on click', async () => {    const plugin = createCroct({      appId: '00000000-0000-0000-0000-000000000000',      test: true,    });
    const wrapper = mount(TrackButton, {      global: {plugins: [plugin]},    });
    const listener = vi.fn();    croct.tracker.addListener(listener);
    await wrapper.find('button').trigger('click');
    expect(listener).toHaveBeenCalledWith(      expect.objectContaining({        status: 'confirmed',        event: {          type: 'goalCompleted',          goalId: 'ctaClicked',        },      }),    );
    croct.tracker.removeListener(listener);  });});

You can find more details about the available events in the Event reference.