Unable to test vue component with v-dialog
Asked Answered
B

5

5

I have been killing myself trying to figure out how to test a Vue component with a v-dialog, something which worked perfectly fine in Vue2. Currently I am using Vue3, Vitest, Vuetify3.

here is a very simple component which demonstrates a problem

<template>
  <div>
    <v-btn @click.stop="dialog=true" class="open-dialog-btn">click me please</v-btn>
    <v-dialog v-model="dialog" max-width="290" >
      <div class="dialog-content">
        <v-card>welcome to dialog</v-card>
      </div>
    </v-dialog>
  </div>
</template>

<script setup>
import {ref} from "vue";

const dialog = ref(false);
</script>

and here is a unit test for it:

import '../setup';
import { mount } from '@vue/test-utils';
import { createVuetify } from "vuetify";
import HelloDialog from "@/components/HelloDialog.vue";

describe('HelloDialog', () => {
  let wrapper;
  let vuetify;

  beforeEach(() => {
    vuetify = createVuetify();
  });

  describe('dialog tests', () => {
    beforeEach(() => {
      wrapper = mount(HelloDialog, {
        global: {
          plugins: [vuetify],
        },
      });
    });

    test('test dialog', async () => {
      expect(wrapper.find('.dialog-content').exists()).toBeFalsy();
      await wrapper.find('.open-dialog-btn').trigger('click');
      console.log(wrapper.html());
      expect(wrapper.find('.dialog-content').exists()).toBeTruthy();
    });
  });
});

the last line in unit test is not working - dialog content is not displayed. Here is an output from wrapper.html() after button is clicked:

<div><button type="button" class="v-btn v-btn--elevated v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-elevated open-dialog-btn"><span class="v-btn__overlay"></span><span class="v-btn__underlay"></span>
    <!----><span class="v-btn__content" data-no-activator="">click me please</span>
    <!---->
    <!---->
  </button>
  <!---->
  <!--teleport start-->
  <!--teleport end-->
</div>

AssertionError: expected false to be truthy
    at ....../HelloDialog.spec.js:27:56

here is test section from vite.config.js:

  test: {
    // https://vitest.dev/config/
    globals:true,
    environment: 'happy-dom',
    setupFiles: "vuetify.config.js",
    deps: {
      inline: ["vuetify"],
    },
  },

and here is vuetify.config.js:

global.CSS = { supports: () => false };

here some versions from package.json:

  "dependencies": {
    "@mdi/font": "7.1.96",
    "@pinia/testing": "^0.0.14",
    "axios": "^1.2.0",
    "dotenv": "^16.0.3",
    "happy-dom": "^8.1.1",
    "jsdom": "^20.0.3",
    "lodash": "^4.17.21",
    "pinia": "^2.0.27",
    "roboto-fontface": "*",
    "vue": "^3.2.45",
    "vuetify": "3.0.6",
    "webfontloader": "^1.0.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.0.0",
    "@vue/test-utils": "^2.2.6",
    "vite": "^4.0.3",
    "vite-plugin-vuetify": "^1.0.0-alpha.12",
    "vitest": "^0.26.2"
  }

I have tried everything at this point, and I think the problem has something to do with v-dialog using teleport component. After struggling for several days trying to figure out I settled on using a stub to not use a real dialog when testing but I really don't like this approach.

any ideas would be greatly appreciated

Bennion answered 1/1, 2023 at 18:41 Comment(3)
Any success here?Creamy
Yes, there are 2 ways to proceed. One option is listed below as an answer. The other option is to create a MockDialog which can be used instead of the dialog component during testing. Once you create a MockDialog component you can replace it using stubs options (VDialog: MockDialog) when mounting a control. I got to say I like using MockDialog more because then I can use all the helper utils I built around vitest/jestBennion
@Bennion can you please share how you were able to get around this? I am having the same issue now and do not fully understand the MockComponent comment. ThanksCommunist
P
3

I have the same issue and found the content of v-dialog was rendered in document.body when I called mount(). You can test the dialog content like below.

// expect(wrapper.find('.dialog-content').exists()).toBeTruthy();
expect(document.querySelector('.dialog-content')).not.toBeNull();

I recommend to call unmount() after each test.

afterEach(() => {
  wrapper.unmount()
});

Hope this helps although I doubt it's a good approach because I don't want to care whether the component is using teleport or not.

Paterson answered 18/1, 2023 at 12:39 Comment(2)
thanks. definitely will try it when I have a minBennion
Your solution definitely works! Thank you. However this takes me outside of vitest framework and it's just a bit harder to do things as I no longer work with a wrapper which provides all kids of utilities but instead of dom elements. I will continue exploring this option.Bennion
L
2

Using Andrey's comment to create a MockDialog stub, I was able to solve my problem. Here's a solution that I used:

test-utils.ts

export function stub(
  tag: string,
  opts ? : {
    [key: string]: any
  },
  template ? : string,
): ComponentPublicInstance < any > {
  const contents = template || `Stubbed ${tag}`;
  return {
    name: pascalCase(tag),
    template: `<div class="${tag}-stub">${contents}</div>`,
    ...(opts || {}),
  };
}

export const sharedStubs = {
  vDialog: stub(
    'v-dialog', {
      props: ['modelValue'],
    },
    '<slot />',
  ),
}

Then, in my *.spec.ts using the v-dialog component:

mount(
  ComponentWithModal, {
    global: {
      plugins: [
        vuetify,
      ],
      stubs: {
        VDialog: sharedStubs.vDialog,
      },
    }
    as GlobalMountOptions,
  })
Lakshmi answered 14/4, 2023 at 18:58 Comment(0)
S
1

A simple approach is to set the attach props to true. So when you mount the component during test, the dialog content will be rendered in the wrapper.

https://vuetifyjs.com/en/api/v-dialog/#props-attach

Such answered 15/2 at 13:51 Comment(0)
B
0

Not sure how the internals of vuetify works for the v-dialog component, but it seems like they use the <Teleport> component, try adding the element where the contents of the teleport will land, the id has to match though for whatever name they are using, check their docs probably they have some props for this

beforeEach(() => {
 const el = document.createElement('div');
 el.id = 'my-teleport';
 document.body.appendChild(el);
});
Bergquist answered 14/3, 2023 at 16:37 Comment(1)
Very nice and clean implementation. I'll try your method as well - I actually crated a stub componentBennion
R
0

The following solution works for me in Vue 2 (jest).

  1. Use this in the test stubs:
stubs: {
      VDialog: {
        name: 'VDialog',
        template: '<div class="v-dialog-stub"><slot /></div>',
        props: ['modelValue'],
      },
}
  1. Trigger the dialog in the test, wait for nextTick:
it('Modal dialog test', async () => {
    const wrapper = mount(MyComponent, options);  // options = test options, including (1.)

    const dialogTrigger = wrapper.find('.my-dialog-trigger');
    dialogTrigger.trigger('click');

    await Vue.nextTick();
    const dialog = wrapper.find('.v-dialog-stub');
    expect(dialog.exists()).toBe(true);
  });
Rear answered 3/5, 2023 at 10:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.