Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Borovez's avatar

Vitest + Inertia.js errors when running tests on Page/Components

To preface, I am very new to writing client-side tests... There doesn't seem to be a lot of discussions/topics/documentation on using/setting up vitest with inertia.js - so I am having some trouble getting it running without errors.

I have setup my project with Laravel + Inertia.js + Vite + Vitest - and everything works fine in terms of compiling with vite - however I am having some trouble figuring out how to setup a basic vitest test for my UserIndex.vue page.

I found a topic related to testing with jest + inertia.js that I based my initial test setup off of.

If you look at my test file demo.test.ts below, you can see that I have setup a vi.mock for the @inertiajs/inertia-vue3 package - this was necessary because without it, it would return an error when trying to render the inertia.js <Head title="test"/> component - which is imported like so: import {Head, Link} from '@inertiajs/inertia-vue3' in my UserIndex.vue page. The same goes for using the inertia helpers like $inertia and $page to access page props and inertia methods for example.

The error was: TypeError: Cannot read properties of undefined (reading 'createProvider')

After, mocking the @inertiajs/inertia-vue3 I was able to bypass that particular error, however I am now running into another error related to the route mixin from inertia that is defined in the app.js - since it missing in the test environment, I am getting the following error regarding _ctx.route is not a function, and warnings about failing to resolve directive tooltip from the floating-vue package. The route() method is used to generate links which comes from the default inertia ziggy routing mixin defined in app.js.

I have found that when running the test using shallowMount instead of mount from @vue/test-utils - it will bypass the errors, but not the warnings, and it will pass the truthy test, but I am unable to get the text from the wrapper like expect(wrapper.text()).toContain('User') - this returns empty text and compares to 'User' which is false. After reading the documentation about mount/shallowMount - it doesn't look like shallowMount is what I need...

So my question is, am I on the right track with this test, should I be testing the entire page this way? If so, what can I do to resolve the route and directive errors? Should I be mocking all of these packages/directives?

Any direction would be very much appreciated. Please let me know if you would like to see any other code/files. Thanks!

_ctx.route error and warnings

stderr | tests/Client/demo.test.ts > UsersIndex > renders
[Vue warn]: Failed to resolve directive: tooltip 
  at <Index users= [] levels= {} ref="VTU_COMPONENT" > 
  at <VTUROOT>
[Vue warn]: Invalid vnode type when creating vnode: undefined. 
  at <Index users= [] levels= {} ref="VTU_COMPONENT" > 
  at <VTUROOT>
[Vue warn]: Property "route" was accessed during render but is not defined on instance. 
  at <Authenticated> 
  at <Index users= [] levels= {} ref="VTU_COMPONENT" > 
  at <VTUROOT>
[Vue warn]: Unhandled error during execution of render function 
  at <Authenticated> 
  at <Index users= [] levels= {} ref="VTU_COMPONENT" > 
  at <VTUROOT>

 ❯ tests/Client/demo.test.ts (1)
   ❯ UsersIndex (1)
     × renders

FAIL  tests/Client/demo.test.ts > UsersIndex > renders
TypeError: _ctx.route is not a function
 ❯ Proxy._sfc_render resources/js/Layouts/Authenticated.vue:94:30
     92|                             <BreezeResponsiveNavLink :href="route('logout')" method="post" as="button">
     93|                                 Log Out
     94|                             </BreezeResponsiveNavLink>
       |                              ^
     95|                         </div>
     96|                     </div>

demo.test.ts

import { assert, describe, expect, it, vi } from 'vitest';
import { config, shallowMount, mount } from '@vue/test-utils';
import UsersIndex from '../../resources/js/Pages/Users/Index.vue';

vi.mock('@inertiajs/inertia-vue3',() => ({
    __esModule: true,
    ...vi.importActual('@inertiajs/inertia-vue3'),  // Keep the rest of Inertia untouched!
    useForm: () => ({
        /** Return what you need **/
        /** Don't forget to mock post, put, ... methods **/
    }),
    usePage: () => ({
        props: {
            value: {
                someSharedData: 'something',
            },
        },
    })
}))

vi.mock('floating-vue', ()=> ({
    __esModule: true,
    ...vi.importActual('floating-vue'),
}));

describe('UsersIndex', () => {
    it('renders', () => {
        const wrapper = mount(UsersIndex, {
            props: {
                users: [],
                levels: {}
            },
            global: {
                // mixins: [{methods: { route }}],
            },
            directives: {
               // tooltip: { }
            }
        })

        expect(wrapper).toBeTruthy()
    })
})

vitest.config.ts. (my test config, which is used only when running vitest)

import { defineConfig } from 'vitest/config';
import { resolve } from 'path';
import vue from '@vitejs/plugin-vue';

export default defineConfig(({ command }) => ({
    publicDir: false,
    build: {
        manifest: true,
    },
    resolve: {
        alias: {
            "@": resolve(__dirname, "resources/js"),
        },
    },
    plugins: [
        vue()
    ],
    test: {
        globals: true,
        environment: 'jsdom'
    }
}));

app.js

import "../css/app.scss";

import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/inertia-vue3';
import { InertiaProgress } from '@inertiajs/progress';
import FloatingVue from 'floating-vue'
import Toast, {TYPE, POSITION} from "vue-toastification";

const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';

let asyncViews = () => {
    return import.meta.glob("./Pages/**/*.vue");
};

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: async (name) => {
        if (import.meta.env.DEV) {
            return (await import(`./Pages/${name}.vue`)).default;
        } else {
            let pages = asyncViews();
            const importPage = pages[`./Pages/${name}.vue`];
            return importPage().then((module) => module.default);
        }
    },
    setup({ el, app, props, plugin }) {
        return createApp({ render: () => h(app, props) })
            .use(plugin)
            .use(FloatingVue)
            .use(Toast)
            .mixin({ methods: { route } })
            .mount(el);
    },
});

InertiaProgress.init({ color: '#4B5563' });

import "./bootstrap";

vite.config.ts

import { defineConfig } from 'vite';
import { resolve } from 'path';
import vue from '@vitejs/plugin-vue';
import laravel from 'vite-plugin-laravel';

export default defineConfig(({ command }) => ({
    publicDir: false,
    build: {
        manifest: true,
    },
    resolve: {
        alias: {
            "@": resolve(__dirname, "resources/js"),
        },
    },
    plugins: [
        vue(),
        laravel()
    ]
}));
0 likes
12 replies
bwilkins's avatar

@borovez

Did you ever figure this out? I'm getting something similar now and was curious what you ended up doing.

cameeob2003's avatar

I'm currently facing this same issue and was wondering if you were able to find any resolution.

Napo7's avatar

Hi,

Same for me :(

I can't find any resource about setting the test environment for Vue and Inertia :(

DokiCRO's avatar

Here is my solution:

In vite.config.js add setpFiles:

test: {
        globals: true,
        setupFiles: ['./vitestSetupFile.js'],
        environment: 'happy-dom',
    },

In vitestSetupFile.js you can add

NOTE: You will have to install ziggy-js package

import route from 'ziggy-js';
import { Ziggy } from '@/ziggy';

config.global.mocks.route = (name) => route(name, undefined, undefined, Ziggy);

And don't forget to generate ziggy.js file with php artisan ziggy:generate

** This needs to be run after modifying routes (I recommend running it on each commit) **

This solves route() issue. Now onto <Head> issue:

Create another component call HeadMocked.vue and make it empty

<template>
    <div>
        Mocked head
    </div>
</template>

Import that component in vitestSetupFile.js

import Head from '@/Components/Shared/MockedHead.vue';
import { config } from '@vue/test-utils';

config.global.components = {
    Head,
};

If you want to use route inside a method you have to use it with window.route() if you want it to work with vitest

const submit = () => {
    form.post(window.route('login'), {
        onFinish: () => form.reset('password'),
    });
};

I hope this helps everybody because I lost like 10hours on figuring this out

3 likes
rc_laracasts's avatar

@DokiCRO I get a "config is not defined" error in vitestSetupFile.js.

Does the config object need to be defined in that file? If so, how'd you accomplish that?

bwrigley's avatar

Sorry to bring this back to life, but I'm not managing to make this work, I still get route is not defined. Or if I change to window.route() I get window.route() is not a function

Could it be how I'm calling it in my script as if I have '{{ route() }}' in my <template> I get no issue:

<script setup>
    import { router } from '@inertiajs/vue3';
    ///
        const methods = {
            toggleArchive(){
                router.visit(route('conversation.toggleArchive',props.conversation.id));
            }
        }
</script>

in my vitestSetupFile:

import { config } from '@vue/test-utils';
import { vi } from "vitest";

import route from 'ziggy-js';
import { Ziggy } from '@/ziggy';

//mocking Lingua
config.global.mocks.__ = (key) => key;

//mocking Ziggy
config.global.mocks.route = (name) => route(name, undefined, undefined, Ziggy);

vi.mock('@inertiajs/vue3', () => ({
    router:{
      visit : (r) => (r),
    }
});
isimmons's avatar

I still get TypeError: Cannot read properties of undefined (reading 'createProvider') if I use mount instead of shallowMount. If I understand correctly, this is because shallowMount will only mount the compoenent being tested but will ignore child components.

This is a problem though. I am looking for screen.findByRole('link') and there are no links. If I console.log(wrapper.html()) I see that every place where there is a Link from inertiajs/vue3 in the actual Welcome.vue component, it is replaced with <link-stub> in the output of wrapper.html()

In <link-stub> the href is filled in correctly but there is no text like 'Dashboard' either. I assume it is passed in via the data=[object, object] which is an attribute on <link-stub>

So I guess we also have to mock Link but I can't figure out how to do that. And I need a fix for the createProvider error so I can use mount instead of shallowMount.

isimmons's avatar

I finally got this figured out. Well at least my first test passes.

Lots of help from you all in this discussion and some help from Copilot in Bing (don't be jealous Larry), to get the full picture.

This is Laravel 11, Vue 3 and the latest of everything else as of May, 28, 2024.

In order to be able to use mount from vue test-utils or render from vue testing library without getting TypeError: Cannot read properties of undefined (reading 'createProvider') I had to mock the $headManager property. I got the clue from this github discussion

This is my testSetupFile.ts

import { config } from '@vue/test-utils';
import { route } from 'ziggy-js';
// @ts-ignore yes Ziggy is there.
import { Ziggy } from '@/ziggy';
import { createHeadManager } from '@inertiajs/core';

//mocking Ziggy
config.global.mocks.route = (name: string) =>
  route(name, undefined, undefined, Ziggy);

// fixes TypeError: Cannot read properties of undefined (reading 'createProvider')
const mockedHeadManager = createHeadManager(
  false,
  () => '',
  () => '',
);
config.global.mocks.$headManager = mockedHeadManager;

And here is my first 2 passing tests using render and screen from testing-library

import '@testing-library/jest-dom';
import { it, expect } from 'vitest';
import { screen, render } from '@testing-library/vue';
import Welcome from '@/Pages/Welcome.vue';

const getSharedPageAuth = (isAuth: boolean = false) => {
  return {
    props: {
      auth: { user: isAuth ? {} : null },
    },
  };
};

it('Shows login/register links if the user is not authenticated', async () => {
  render(Welcome, {
    props: {
      canLogin: true,
      canRegister: true,
      laravelVersion: '11',
      phpVersion: '8.3',
    },
    global: {
      mocks: { $page: getSharedPageAuth() },
    },
  });

  expect(screen.getByRole('link', { name: /log/i })).toBeInTheDocument();
  expect(screen.getByRole('link', { name: /register/i })).toBeInTheDocument();
});

it('Shows a dashboard link if the user is authenticated', async () => {
  render(Welcome, {
    props: {
      canLogin: true,
      laravelVersion: '11',
      phpVersion: '8.3',
    },
    global: {
      mocks: { $page: getSharedPageAuth(true) },
    },
  });

  expect(screen.getByRole('link', { name: /dashboard/i })).toBeInTheDocument();
});
2 likes
bambamboole's avatar

@isimmons can you also post your package.json file and vite.config.ts file please? I have some issues reproducing it.

isimmons's avatar

@bambamboole Sure. Here is the entire project ld-forum

Sorry, I got off on other things and haven't messed with this in a while. I just cloned it and ran tests to make sure it is working.

Tests are in resources/js/tests along with setup.ts.

Hope it helps :-)

1 like

Please or to participate in this conversation.