vuejs / test-utils

Vue Test Utils for Vue 3
https://test-utils.vuejs.org
MIT License
1.02k stars 245 forks source link

Docs: trigger custom event #885

Closed scvnc closed 2 years ago

scvnc commented 3 years ago

As a Vue3 newbie, coming from React land. I wanted to trigger a custom event and assumed I could do something like wrapper.trigger('my-custom-event')

It seems this does not work for custom events and is only for DOM events. Digging a bit I see that others have accomplished this with wrapper.vm.$emit

standbyoneself commented 3 years ago

Using wrapper.vm.$emit() is the only one way to trigger custom events in tests. And it is correct, because vm refers to component instance (ViewModel). When you trigger the event like this (programmatic way), you would not get any errors even if there is no this event in emits option.

lmiller1990 commented 3 years ago

wrapper.trigger('my-custom-event')

Is this something you can do in React? I also come from React land, and am generally using Testing Library, where you are also limited to DOM events using fireEvent.[event]. I feel like testing custom events by using vm.$emit is nearing an implementation details, but happy to explore your use case if you can share one.

Can you share the component/test you'd like to write? Generally I recommend interacting with your component like a user would, for example by triggering native events on HTML elements (which then might lead to a method calling emit('some-custom-event').

If you really wanted a triggerCustom, it would probably be doable in userland via a plugin.

scvnc commented 3 years ago

Example Component

describe("MessageListView", () => {
  describe("when a message is confirmed for deletion", () => {
    beforeEach(() => {
      const wrapper = mount(MessageListView);
      wrapper.findComponent(MessageListItem).trigger("deleted", { id: 3 });
    });

    it("should tell the message service to delete the id", () => {
      expect(mockedMsgSvc.deleteById).toHaveBeenCalledwith(3);
    });
  });
});

describe("MessageListItem", () => {
  describe("when the trash icon is clicked", () => {
    it('should show a "are you sure" pill', () => {
      // ..
    });

    describe('when the green checkmark on the pill is clicked', () => {
      it("should emit the deleted event with the given id", () => {});
    });
  });
});

React Flavor

describe("MessageListView", () => {
  describe("when a message is confirmed for deletion", () => {
    beforeEach(() => {
      const wrapper = mount(MessageListView);

      // The richer UI interaction is just an implementation detail of MessageListItem
      // It boils down to this call, 'event'.  I've read that Vue prefers events vs
      // function passing thru props.
      wrapper.findComponent(MessageListItem).invoke("onDeleted", 3);
    });

    it("should tell the message service to delete the id", () => {
      expect(mockedMsgSvc.deleteById).toHaveBeenCalledwith(3);
    });
  });
});

describe("MessageListItem", () => {
  describe("when the trash icon is clicked", () => {
    it('should show a "are you sure" pill', () => {
      // ..
    });

    describe('when the green checkmark on the pill is clicked', () => {
      it("should call the onDeleted prop with the given id", () => {
        // ..
      });
    });
  });
});

I was in Enzyme land before Testing Utils came along... it does seem tricky if your testing library isn't aware of the view framework internals. I suppose I could swap out MessageListItem for a mock component which emits delete when it's clicked or something that could be triggered with a dom event.

Using composed wrapper methods

If not trying to test components as units and their interactions -- I suppose digging down into the component interactions with some wrapper functions could be a strategy.

function confirmMessageForDeletion(wrapper) {
  // This works, but now we depend on the implementation of MessageListItem
  // and this test could break if the implementation of it changes.
  wrapper.trigger('click')
  wrapper.find('.green-confirm-btn').trigger('click')
}

describe("MessageListView", () => {
  describe("when a message is confirmed for deletion", () => {
    beforeEach(() => {
      const wrapper = mount(MessageListView);

      confirmMessageForDeletion(MessageListItem)
    });
    // ..

This may not be so bad if the interaction to trigger the custom event is simple. I could see it getting out of hand if it's more complicated like an spreadsheet data grid or maybe some canvas based image cropping component.

The idea for introspecting whether the component can emit

With React, I can have TypeScript templates where if I change the name of a prop or 'event', I will get compiler errors. So I'm seeking out tooling such as that which enhances that angle of protection while saving the need for manual test cases. Writing a plugin sounds like a nice way to explore this :)

lmiller1990 commented 3 years ago

Here's an example PR showing how I'd generally approach this kind of test. It is using the new script setup syntactic sugar. It allows for improved type safety with defineEmits and defineProps. There are two tests; one is more ideal, imo - no implementation details. We test like a user - clicking the button. The second shows how you might call the internal handle method (eg, trigger a custom event, I guess) using vm (which is the internal Vue instance for the component).

https://github.com/vuejs/vue-test-utils-next/compare/issue-885-example?expand=1

While it is possible to pass a callback as a prop in Vue, a more common approach is to respond to an event. I showed that in my above example - you can use emitted if you really would like to see the internally emitted events. Generally, I'd recommend against tests like "it calls the XXX prop" - this is an implementation detail. What you really want to test is what happens when XXX prop is called - in this scenario, the delete service is called with the correct argument. The same thing applies in React - I'd write this test with the same ideas and assertions for React using Testing Library.

... it does seem tricky if your testing library isn't aware of the view framework internals ...

Ideally, a framework should not be aware of internals. It's tricky because generally internals aren't exposd - they are internals, after all. While Enzyme supported this, a lot of those code bases did not age well and the test suites were very brittle. Testing implementation details is generally a not a good idea. Those test are brittle and are likely to break during a refactor. This is the reason libraries like Testing Library don't expose the underlying internals.

Re: type safety

You might see I had to ts-ignore a bit - unless you are using TSX (which is very possible, we have some simple examples but you can google more on how to configure TSX) you won't get the same level of completion as React when importing a vue file, at least not yet. A lot of work is getting done using the VS Code extension Volar to support this, though.

scvnc commented 2 years ago

Thanks for making that @lmiller1990. Some pretty cool stuff with the script setup syntactic sugar and some typescript love.

I'm not entirely satisfied with the solution at a glance and would need to pull and play with it some (in time)

Source of my issue in tests/components/DeleteMessageList.spec.ts:

await wrapper.get('button').trigger('click')

All this tests is that when I click a button (assuming that this component would actually render many buttons,) that it would emit the correct event. The consuming component of this list view doesn't care that it's a button that is being clicked or the like. I'd likely need to test the correct button(s) it rendered to ensure that they are all wired up correctly... but then I'm testing implementation details 😞 ...

Lets take another potentially clearer example, like a special canvas based component which allows refined editing of an audio clip <audio-editor src="uri/to/file.mp3" />. When the user manipulates the canvas-based editor and invokes the "save" function, I'd want my vue app to capture that and call the appropriate service to actually perform the save. In this case <audio-editor> would emit a custom event save-file with { blobHandle, filename}. If I had some more utilities, I could begin to do something like wrapper.moveMouseOverSaveIconAndClick() but I would be testing implementation details of the audio editor in that case and I'd rather use Cypress or something similar. I just need some way of emitting the event that the audio editor would emit during this scenario. I could do that like this:

wrapper.findComponent(AudioEditor).vm.$emit("save-file", { blobHandle, filename });

Ah, ha, but in the next version of AudioEditor, they broke the contract and now it's called "saveLastEdit" and doesn't use blobHandle and instead audioB64... in this case, I am causing the AudioEditor to trigger an event it would never emit and my test will happily pass... and i would like a failing test... So I'm looking for something that would cause that to blow up, either from typescript or a test util, or some other best practice strategy for handling this case.

I can begin to interpret from your example that a good practice for <audio-editor> would be to define a public method that is expected to be called by testing code. audioEditor.vm.emitSaveEvent({ blobHandle, fileName}) and hopefully it would blow up if the testing code causes it to emit something invalid. Or potentially there should be a testing utility library that is owned by AudioEditor that is maintained to cause it to only emit values it would emit in practice. AudioEditorTestUtils.emitSave(AudioEditorwrapper, { blobHandle, fileName})

I want to inspect your examples a bit more thoroughly. Just writing this here to note that I haven't forgotten. Thank you for your collaboration 😄

lmiller1990 commented 2 years ago

... Ah, ha, but in the next version of AudioEditor ...

The only way to catch something like this would be to test against the actual AudioEditor component - so basically, an integration test.

It seems you can accomplish what you want via wrapper.vm.$emit? Is there any action to be taken here? Perhaps we should close this and continue using the new "discussion" feature (enabled in this repo).

julisch94 commented 4 months ago

I have come across the same issue and found this conversation.

I wanted to trigger a custom event change with the string foo but

myComponent.trigger('change', 'foo')

yields this:

event triggered! value: Event {
  '0': 'f',
  '1': 'o',
  '2': 'o',
  isTrusted: [Getter],
  _vts: 1714118428122
}

which is unacceptable.

A workaround for now is:

myComponent.vm.$emit('change', 'foo')

which yields as expected:

event triggered! value: foo

Why doesn't it work with triggered? What am I doing wrong?