Skip to content

Bug: Spy is not called if you don't add parentheses to the function handler #2066

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
6 tasks done
lobo-tuerto opened this issue Sep 21, 2022 · 9 comments
Closed
6 tasks done

Comments

@lobo-tuerto
Copy link

lobo-tuerto commented Sep 21, 2022

Describe the bug

Right now if you have a child component that handles an emitted event by calling a function in the parent component, it will just fail if you don't add parenthesis to the event handler name.

To Reproduce
This doesn't work:

<MainSidebar
  :menu-items="menuItems"
  @expanded="handleExpanded"
/>

This works:

<MainSidebar
  :menu-items="menuItems"
  @expanded="(value) => handleExpanded(value)"
/>

I just found out about this unexpected behaviour by chance.
It's weird because in the Vue docs they use the non-parentheses version all over the place.

Expected behavior
This should work:

<MainSidebar
  :menu-items="menuItems"
  @expanded="handleExpanded"
/>

Related information:

This is with a newly created Vue 3 + Vite + TypeScript application (pnpm create vite).

Additional context

Here is a simple example:

test('MainSidebar events', async () => {
  const wrapper = mount(HomePage, {
    global: {
      plugins: [router],
    },
  })

  const spy = vi.spyOn(wrapper.vm, 'handleExpanded')

  const component = wrapper.findComponent(MainSidebar)
  component.vm.$emit('expanded')

  expect(component.emitted().expanded).toBeTruthy()
  expect(spy).toHaveBeenCalled()
})

That's the gist of it.

Alternative Solutions

For now, use parenthesis on all event handlers and manually pass any data to it... :(
Add information about this issue in a very visible place on the Vitest docs (didn't find anything about it when going through the site, it should be a big yellow warning box). :)

Previous discussions

vuejs/test-utils#1769 (most recent one)

vuejs/vue-test-utils#1901
vuejs/vue-test-utils#929
https://stackoverflow.com/questions/62696939/test-on-function-call-in-vue-template-only-passes-if-the-function-is-called-with/62703070#62703070

Reproduction

I have repo with a brand new Vite + Vue 3 app that exhibits the behaviour discussed above right here: https://github.com/lobo-tuerto/test-utils-parentheses

Please clone it, install dependencies (I'm using pnpm), then run: pnpm vitest run

You'll see:

stdout | src/App.test.ts > event handling
this was called!

 ❯ src/App.test.ts (1)
   × event handling

 FAIL  src/App.test.ts > event handling
AssertionError: expected "someEventHandler" to be called at least once
 ❯ src/App.test.ts:17:14
     15| 
     16|   expect(component.emitted().something).toBeTruthy()
     17|   expect(spy).toHaveBeenCalled()
       |              ^
     18| })
     19| 

Test Files  1 failed (1)
     Tests  1 failed (1)
  Start at  00:30:01
  Duration  2.66s (transform 429ms, setup 0ms, collect 114ms, tests 22ms)

Please note that this line in src/App.vue does not have parentheses in its event handler:

<HelloWorld msg="Vite + Vue" @something="someEventHandler" />

Now, add parentheses to it like this:

<HelloWorld msg="Vite + Vue" @something="someEventHandler()" />

Run pnpm vitest run again and you'll see:

stdout | src/App.test.ts > event handling
this was called!

 ✓ src/App.test.ts (1)

Test Files  1 passed (1)
     Tests  1 passed (1)
  Start at  00:34:10
  Duration  2.46s (transform 1.07s, setup 0ms, collect 114ms, tests 21ms)

That's basically the issue.
Hope this helps troubleshoot what's going on.

Thank you in advance.

System Info

OS: Manjaro Linux
Node: v18.9.0


  "dependencies": {
    "vue": "^3.2.39"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^3.1.0",
    "@vue/test-utils": "^2.0.2",
    "jsdom": "^20.0.0",
    "typescript": "^4.8.3",
    "vite": "^3.1.2",
    "vitest": "^0.23.4",
    "vue-tsc": "^0.40.13"
  }

Used Package Manager

pnpm

Validations

@sheremet-va
Copy link
Member

This is expected behavior. It's impossible to track down a spy, when it looses its parent object. It's not Vue problem, this is how spying on an object works.

I would recommend not using this pattern with spies when testing web components. You are testing implementation details instead of actual interactions with DOM.

Here is a good read on the subject: https://kentcdodds.com/blog/testing-implementation-details

@lobo-tuerto
Copy link
Author

I'll have a read, thanks for the recommendation! 👍

@lobo-tuerto
Copy link
Author

People at test-utils mentioned this, though:

I suppose there is something going on with the spy that can't catch the call to the listener if there are no parenthesis as the code generated by Vue is { onClick: onClick } for @click="onClick" and { onClick: $event => (onClick()) } for @click="onClick()"
Maybe Vue itself should generate { onClick: $event => (onClick()) } in all cases, I don't know

@lobo-tuerto
Copy link
Author

Maybe documenting the behaviour could be a good starting point?
It was not easy to find out what was going on, or what was the problem at first. 🤔

@sheremet-va
Copy link
Member

This is how mocking works in JavaScript. I am not sure what do you want to document? You are applying a mock to an object's method too late - the "onClick" in {onClick: onClick} already rendered, so when you put a mock on vm it mocks a METHOD on vm, but this function (onClick) is not part of vm anymore. They are the same functions (onClick === vm.onClick), but mock can only track calls on vm.

@lobo-tuerto
Copy link
Author

How comes it's already rendered when written without parentheses, and is not already rendered when using parentheses? 🤔

@sheremet-va
Copy link
Member

Because it's a function call, it's not evaluated yet.

@lobo-tuerto
Copy link
Author

Then as the test-utils team mentioned, it would make sense for Vue to always generate { onClick: $event => (onClick()) } in all cases? (parentheses or no parentheses).

@sheremet-va
Copy link
Member

Then as the test-utils team mentioned, it would make sense for Vue to always generate { onClick: $event => (onClick()) } in all cases? (parentheses or no parentheses).

I guess. It's not up to Vitest how vue generates js 🤷🏻‍♂️


This code is already evaluated, so when you are spying on vm it's too late, original function is already on another object:

const vm = {
  fn() {},
  template: `<div @click="fn" />`
}
render(vm) // produses, let's say { onClick: vm.fn }
vi.spyOn(vm, 'fn') // template is already evaluated, and pointer is lost basically 

Here you always have access to it:

const vm = {
  fn() {},
  template: `<div @click="fn()" />`
}
render(vm) // produses, let's say { onClick: () => vm.fn() }
vi.spyOn(vm, 'fn') // function is not evaluated yet, and you will always have access to vm.fn there 

@sheremet-va sheremet-va closed this as not planned Won't fix, can't repro, duplicate, stale Sep 22, 2022
@github-actions github-actions bot locked and limited conversation to collaborators Jun 12, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants