Search code examples
nuxt.jssupabasevitest

How to mock Supabase in Vitest?


I've got a Nuxt3 application that uses the @nuxtjs/supabase module for auth and storage, and am trying to add Vitest unit testing to it.

In my app.vue, I have the following:

<script lang="ts" setup>
  import type { Database } from '@/types';

  const supabase = useSupabaseClient<Database>();
  supabase.auth.onAuthStateChange(async (event) => {
    if (event === 'SIGNED_OUT') {
      await supabase.removeAllChannels();
    }
  });
</script>

<template>
  <div class="bg-white h-full">
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>

And in my tests/unit/app.spec.ts:

import { mountSuspended } from '@nuxt/test-utils/runtime';
import { describe, expect, it, vi } from 'vitest';
import app from '@/app.vue';

// const supabaseClientMock = vi.fn(() => ({
//   auth: {
//     onAuthStateChange: vi.fn(),
//   },
//   realtime: {
//     removeAllChannels: vi.fn(),
//   },
// }));
// vi.stubGlobal('useSupabaseClient', supabaseClientMock);

// vi.mock('#imports', () => {
//   return {
//     useNuxtApp: () => ({
//       $supabase: {
//         client: {
//           auth: {
//             onAuthStateChange: vi.fn(),
//             signIn: vi.fn(),
//             signOut: vi.fn(),
//           },
//           realtime: {
//             removeAllChannels: vi.fn(),
//           },
//           // Add other properties and methods as needed
//         },
//       },
//     }),
//   };
// });

const supabaseClientMock = vi.fn(() => ({
  auth: {
    onAuthStateChange: vi.fn(),
  },
  realtime: {
    removeAllChannels: vi.fn(),
  },
}));

vi.stubGlobal('useSupabaseClient', supabaseClientMock);

describe('App', async () => {
  describe('App Component', async () => {

    it('renders the app', async () => {
      const wrapper = await mountSuspended(app);
      expect(wrapper.exists())
        .toBe(true);
    });
  });
});

As you can see, I've attempted many different variations, however I keep running in to the same error:

TypeError: Cannot read properties of undefined (reading 'auth')
 ❯ setup app.vue:7:7
      5|   supabase.auth.onAuthStateChange(async (event) => {
      6|     if (event === 'SIGNED_OUT') {
      7|       await supabase.removeAllChannels();
       |       ^
      8|     }
      9|   });
 ❯ wrappedSetup node_modules/@nuxt/test-utils/dist/runtime-utils/index.mjs:93:26
 ❯ clonedComponent.setup node_modules/@nuxt/test-utils/dist/runtime-utils/index.mjs:141:48
 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:195:19
 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7572:25
 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7533:36
 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5861:7
 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5827:9
 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5306:11
 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5971:11

This error originated in "tests/unit/app.spec.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
The latest test that might've caused the error is "renders the app". It might mean one of the following:
 - The error was thrown, while Vitest was running this test.
 - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Ideally I'd be able to move this into a file to be used in something like setupFiles rather than mocking Supabase in every test file, however if I just need to get it working first!

For what it's worth, I did get a version of it working correctly in another test file, which uses Supabase within a helper function, but I'm unable to replicate it in the app spec:

vi.mock('@/utils/helpers', () => {
  const buildImageSrc = vi.fn((url: string) => url);

  return {
    useSupabaseClient: () => ({
      storage: {
        from: () => ({
          getPublicUrl: (url: string) => ({
            data: { publicUrl: url },
          }),
        }),
      },
    }),
    buildImageSrc,
  };
});

Solution

  • For posterity, I think I figured it out:

    const { useSupabaseClient } = vi.hoisted(() => {
      return {
        useSupabaseClient: vi.fn()
          .mockImplementation(() => {
            return {
              auth: {
                onAuthStateChange: vi.fn(),
              },
              realtime: {
                removeAllChannels: vi.fn(),
              },
              storage: {
                from: () => ({
                  getPublicUrl: (url: string) => ({
                    data: { publicUrl: url },
                  }),
                }),
              },
            };
          }),
      };
    });
    mockNuxtImport('useSupabaseClient', () => useSupabaseClient);