vuejs / vue-test-utils

Component Test Utils for Vue 2
https://vue-test-utils.vuejs.org
MIT License
3.57k stars 670 forks source link

Triggering mouseenter event on v-tooltip does not update the wrapper #1421

Closed begueradj closed 4 years ago

begueradj commented 4 years ago

Version

1.0.0-beta.31

Reproduction link

https://github.com/begueradj/test-vuetify-tooltip

Steps to reproduce

  1. In pages/index.vue:
<template>
  <component-with-tooltip tooltipText="Some information" />
</template>

<script>
import ComponentWithTooltip from '@/components/ComponentWithTooltip.vue'
export default {
  components: { ComponentWithTooltip }
}
</script>
  1. In components/ComponentWithTooltip.vue:
<template>
  <v-container>
    <v-row>
      <v-col cols="12">
        <v-tooltip bottom>
          <template v-slot:activator="{ on }">
            <v-icon v-on="on" color="grey">
              help
            </v-icon>
          </template>
          <span>
            {{ tooltipText }}
          </span>
        </v-tooltip>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
export default {
  name: 'ComponentWithTooltip',
  props: {
    tooltipText: {
      type: String,
      default: ''
    }
  }
}
</script>
  1. In test/components/ComponentWithTooltip.vue:
it('2. User interface provides one help icon with tooltip text', async () => {
    const icons = wrapper.findAll('.v-icon')
    expect(icons.length).toBe(1) // Ok

    const helpIcon = icons.at(0)
    expect(helpIcon.text()).toEqual('help') // Ok

    helpIcon.trigger('mouseenter')
    await wrapper.vm.$nextTick()
    expect(wrapper.text()).toEqual('science') // why this fails ?
  })

What is expected?

I expect when I trigger the mouseenter event in my test, the wrapper should be updated to show a new text which is the one I defined for the prop.

What is actually happening?

WHen I trigger the mouse event, the wrapper keeps its former state, meaning its only available text is related to the help icon text.

Thank you, Billal BEGUERADJ

dobromir-hristov commented 4 years ago

Hey there, the link to your repo is busted :/ Also, does your v-tooltip have a Transition by any chance? We have noticed that those cause issues sometimes.

lmiller1990 commented 4 years ago

VTooltip [uses transitions(https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/components/VTooltip/VTooltip.ts).

Can you try:

mount({
  stubs: {
    transition: true,
  }
})

and see if that works?

begueradj commented 4 years ago

@dobromir-hristov The link I provided is not broken. I am not using transition, but by default it is null.

begueradj commented 4 years ago

@lmiller1990 Good point. I did that but still I do not get the tooltip text:

it('7. Displays help text on mouse hover', async () => {
  const helpIcon = wrapper.find('.v-icon')
  expect(helpIcon.exists()).toBe(true)
  helpIcon.trigger('mousenter')
  await wrapper.vm.$nextTick()
  console.log(wrapper.text()) // tooltip text not shown here
})
lmiller1990 commented 4 years ago

@begueradj the link in the first post is 404ing. Can you check it? What is the link to your repo?

lmiller1990 commented 4 years ago

Ok, I figured the problem out. It's because VTooltip uses requestAnimationFrame.

I got the test to pass:

import { mount } from '@vue/test-utils'
import Comp from '../../src/TooltipComp.vue'

it('2. User interface provides one help icon with tooltip text', async () => {
  const wrapper = mount(Comp)
  const icons = wrapper.findAll('.v-icon')
  expect(icons.length).toBe(1) // Ok

  const helpIcon = icons.at(0)
  expect(helpIcon.text()).toEqual('help') // Ok

  helpIcon.trigger('mouseenter')
  await wrapper.vm.$nextTick()
  requestAnimationFrame(() => {
    expect(wrapper.text()).toEqual('science') // why this fails ?
  })
})

Try that.

I don't think is something we can easily fix in VTU.

begueradj commented 4 years ago

I am sorry, the repo I linked to was indeed invisible because I set it to be private. Now it is public. I will try your new approach. Thank you very much for the efforts and informatoin.

begueradj commented 4 years ago

@lmiller1990

This gives a false positive:

 requestAnimationFrame(() => {
   expect(wrapper.text()).toEqual('science') // test passes whatever text I inject here
 })

I will try this differently and I will update here. Thank you

lmiller1990 commented 4 years ago

Oh, you need done so Jest waits for the update:

// added done callback to the function
it('2. User interface provides one help icon with tooltip text', async (done) => {
  // stuff
  helpIcon.trigger('mouseenter')
  await wrapper.vm.$nextTick()
  requestAnimationFrame(() => {
    // assert
    done() // <- here
  })
})

Note both help and science will still be in the markup, from the looks of things (Vuetify does not seem to remove the template from the markup, just hide it somehow. Add a selector to the span:

          <span id="custom-tooltip">
            {{ tooltipText }}
          </span>

For example.

begueradj commented 4 years ago

Thank you again :+1: I still can not access the tooltipText value text on mouseenter event.

lmiller1990 commented 4 years ago

I think I am missing something. This works for me (added id and requestAnimationFrame). Removing mousenter makes it fail, which seems correct. Is there any other problem?

<template>
  <v-container>
    <v-row>
      <v-col cols="12">
        <v-tooltip bottom>
          <template v-slot:activator="{ on }">
            <v-icon v-on="on" color="grey">
              help
            </v-icon>
          </template>
          <span id='tooltip-text'>
            {{ tooltipText }}
          </span>
        </v-tooltip>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
export default {
  name: 'ComponentWithTooltip',
  props: {
    tooltipText: {
      type: String,
      default: ''
    }
  }
}
</script>
import Vuetify from 'vuetify'
import { mount } from '@vue/test-utils'
import ComponentWithTooltip from '@/components/ComponentWithTooltip.vue'

const vuetify = new Vuetify()
let wrapper = null

beforeEach(() => {
  wrapper = mount(ComponentWithTooltip, {
    vuetify,
    propsData: {
      tooltipText: 'science'
    }
  })
})

afterEach(() => {
  wrapper.destroy()
})

describe('ComponentWithTooltip.vue:', () => {
  it('1. Mounts properly', () => {
    expect(wrapper.isVueInstance()).toBe(true) // Ok
  })

  it('2. User interface provides one help icon with tooltip text', async (done) => {
    const icons = wrapper.findAll('.v-icon')
    expect(icons.length).toBe(1) // Ok

    const helpIcon = icons.at(0)
    expect(helpIcon.text()).toEqual('help') // Ok

    helpIcon.trigger('mouseenter')
    await wrapper.vm.$nextTick()
    requestAnimationFrame(() => {
      expect(wrapper.find('#tooltip-text').text()).toEqual('science') // why this fails ?
      done()
    })
  })
})
begueradj commented 4 years ago

Yes, that is exactly what I did (I read your comments carefully). As I said before, this line:

expect(wrapper.find('#tooltip-text').text()).toEqual('sddjkdkjdkje') 

gives a false positive: any text other than science will be accepted.

Even not setting an id to the span element gives a false positive. I thank you for the efforts though.

lmiller1990 commented 4 years ago

That's not the case when I run it:

● ComponentWithTooltip.vue: › 2. User interface provides one help icon with tooltip text

    expect(received).toEqual(expected) // deep equality

    Expected: "sddjkdkjdkje"
    Received: "science"

      34 |     await wrapper.vm.$nextTick()
      35 |     requestAnimationFrame(() => {
    > 36 |       expect(wrapper.find('#tooltip-text').text()).toEqual('sddjkdkjdkje') // why this fails ?
         |                                                    ^
      37 |       done()
      38 |     })
      39 |   })

Anything other than science fails (so it should).

Or I misunderstood something - I can push a repo with my changes if you like.

begueradj commented 4 years ago

Indeed, I had a typo, your solution works perfectly well on the example I shared.

Thank you so much for your efforts and attention. Have a nice Sunday :+1: @lmiller1990

lmiller1990 commented 4 years ago

No problem, glad you got it working

6LpUkQSgQm commented 3 years ago

Hello @lmiller1990, why does your example not work?

My component:

<template>
  <div class="ml-4">
    <v-tooltip v-if="!$vuetify.theme.dark" bottom>
      <template v-slot:activator="{ on }">
        <v-btn elevation=2 small fab rounded v-on="on" color="primary" @click="darkMode">
          <v-icon color="primary">mdi-moon-waxing-crescent</v-icon>
        </v-btn>
      </template>
      <span>Dark Mode On</span>
    </v-tooltip>
    <v-tooltip v-else bottom>
      <template v-slot:activator="{ on }">
        <v-btn elevation=2 small fab rounded v-on="on" color="secondary" class="darkmode-button" @click="darkMode">
          <v-icon color="yellow">mdi-white-balance-sunny</v-icon>
        </v-btn>
      </template>
      <span>Dark Mode Off</span>
    </v-tooltip>
  </div>
</template>

<script>
import {mapActions} from 'vuex';
export default {
  name: "DarkModeButton",
  methods: {
    ...mapActions(['updateTheme']),
    darkMode() {
      this.$vuetify.theme.dark = !this.$vuetify.theme.dark;
      this.updateTheme(this.$vuetify.theme.dark);
    },
  },
};
</script>

My test :

import Vue from "vue";
import Vuetify from "vuetify";
import Vuex from "vuex";
import DarkModeButton from "../../components/navigation/DarkModeButton.vue";
import { createLocalVue, mount } from "@vue/test-utils";

describe("DarkModeButton.vue", () => {
  const localVue = createLocalVue()
  let vuetify;
  let wrapper;
  Vue.use(Vuetify);
  beforeEach(() => {
    vuetify = new Vuetify();
  });
  afterEach(() => {
    wrapper.destroy()
  })
  it("1.Find component", () => {
      wrapper = mount(DarkModeButton, {
      localVue,
      vuetify
    });
    expect(wrapper.findComponent({ name: "DarkModeButton" }).exists()).toBe(true);
  });
  it("2.Theme light by default", () => {
    wrapper = mount(DarkModeButton, {
      localVue,
      vuetify,
    });
    expect(wrapper.find('.mdi-moon-waxing-crescent').exists()).toBe(true)
  });
  it("3.Tootlip Dark Mode On", async (done) => {
    const localVue = createLocalVue();
    localVue.use(Vuex); 
    wrapper = mount(DarkModeButton, {
      localVue,
      vuetify
    }); 
    const vIcon = wrapper.find('.v-icon')
    vIcon.trigger('mouseenter');
    await wrapper.vm.$nextTick()
    requestAnimationFrame(() => {
      expect(wrapper.find('span').text()).toBe('Dark Mode On')
      done()
    })
  });
});