Skip to content
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

Bug: findComponent doesn't not return exposed properties in vm if component is bundled #2591

Open
renatodeleao opened this issue Jan 9, 2025 · 4 comments
Labels
bug Something isn't working

Comments

@renatodeleao
Copy link
Contributor

renatodeleao commented Jan 9, 2025

Describe the bug
This is a bit hard to describe but hopefully the reproduction demo will clear things up.
I have a big project with 7000+ thousand tests. Recent ones are good, old ones are bad and rely heavily on .vm.

I'm migrating the code base to a monorepo and the design system is now bundled as separate package by vite and the apps import the components’ bundled version instead of the .vue, tests inherently do too. I noticed that most of the tests that rely on vm stopped working.

wrapper.findComponent(VComp).vm.someMethod // someMethod is always undefined, before bundle it wasn't.

Note

I don't know if this is VTU fault or if it's by design, but having in mind that if I import the raw .vue component instead of the bundled .js version everything works I am assuming it is — Please close this issue if my assumption is wrong.

To Reproduce
https://stackblitz.com/edit/vitest-dev-vitest-h8drorow?file=apps%2Fdemo%2Ftest%2Fbasic.test.ts
Should run automatically, if not npm run test

Expected behavior
exposed properties via defineExpose are available though of findComponent returned VueWrapper

Assuming a basic VExample.vue component

<!-- packages/ui/src/VExample.vue -->
<template>
  <div>Example</div>
</template>

<script setup>
const props = defineProps({
  a: String,
});

const exposedFn = () => {
  console.log('hey');
};

defineExpose({
  exposedFn,
});
</script>

Bundles to

import { openBlock as o, createElementBlock as p } from "vue";
const l = {
  __name: "VExample",
  props: {
    a: String
  },
  setup(n, { expose: e }) {
    return e({
      exposedFn: () => {
        console.log("hey");
      }
    }), (s, t) => (o(), p("div", null, "Example"));
  }
};
export {
  l as VExample
};

Faulty test

import { VExample as VExampleRaw } from '../../packages/ui/src/VExample.vue'
import { VExample as VExampleBundled } from 'ui'

// swapping VExampleBundled per VExampleRaw in the components option makes it pass.
it('finds exposed "vm" properties in *bundled* component wrapper via findComponent', () => {
  const wrapped = mount({
    components: {
      VExampleBundled,
    },
    template: `<div><v-example /></div>`,
  });

  const test = wrapped.findComponent({ name: 'VExample' });

  console.log(test.vm); // only has "a" prop
  expect(test.exists()).toBe(true);
  // fails
  expect(test.vm.exposedFn).toBeInstanceOf(Function);
});

Related information:

vue:  3.5.13 
@vue/test-utils: 2.4.6

Additional context
The same behaviour is not found in mount. When passing the bundled version of a component to mount, exposed properties are exposed correctly to vm, that's why I think it's a bug in findComponent.

@renatodeleao renatodeleao added the bug Something isn't working label Jan 9, 2025
@cexbrayat
Copy link
Member

This indeed looks like a bug if it works with mount but not with findComponent.
I suppose that if you add a unit-test with the bundle version in the codebase you should be able to reproduce the issue and you might be able to fix findComponent

@renatodeleao
Copy link
Contributor Author

@cexbrayat ok, i'll give it a try :)

@renatodeleao
Copy link
Contributor Author

renatodeleao commented Jan 10, 2025

Ok got my first clue, might be a regression from #2327, reverting make mine pass. Will try to do a fix that please both scenarios.

Not that, made a very hacky workaround. Will do a PR but I don't know how to fix it "properly".

Nope still doesn't work for all cases.

renatodeleao added a commit to renatodeleao/test-utils that referenced this issue Jan 10, 2025
renatodeleao added a commit to renatodeleao/test-utils that referenced this issue Jan 10, 2025
…the vm

even when that instance does not have a templateRef?
@renatodeleao
Copy link
Contributor Author

renatodeleao commented Jan 11, 2025

@cexbrayat i've spend way more time on this than I should and yet I don't think I know exactly why it happens. 😛

Why mount "correctly" includes every exposed property in vm

I believe the reasons why the mount function always work is one of these — but i don't know
enough of vue core to be 100% sure.

  1. because the component instance is wrapped in a VTU_ROOT that uses a templateRef on the target component, so exposed the properties become available on the componentPublicInstance by design of (expose/defineExpose) — as per docs.
  2. because the component instance provide is the ComponentInternalInstance

The findComponent case

Back to the .findComponent(SomeComponent) case, the returned instance that is going to be passed to VueWrapper class constructor is the proxy and since we didn't use a templateRef the exposed properties are not available through the ComponentPublicInstance. If VTU wants to include exposed properties in the .vm it needs to go into the $parent/$root chain to get it and assign it. Again, not sure if this is "against the rules", it's in briefly mentioned in docs of exposed/defineExpose as a way to access them

To the code

From my tests findComponent call on components enters this block of the function and calls findAllComponents

const [result] = this.findAllComponents(selector)
return result ?? createWrapperError('VueWrapper')

And there, we return the proxy to createVueWrapper which does not have the exposed properties by design **if using <script setup>. I know this because before calling .map results is a ComponentInternalInstance[] which has all the properties including exposed properties. but we need to provide the proxy to createVueWrapper here or a lot of tests will fail.
const results = find(currentComponent.subTree, selector)
return results.map((c) =>
c.proxy
? createVueWrapper(null, c.proxy)
: createDOMWrapper(c.vnode.el as Element)
)
}

I've made a hacky fix draft by assining the exposed properties via root chain to this.componentVM or via createVMProxy and added tests and double check tests for all scenarios (even added the mount tests (which I believe you kind already do in expose.spec.js) because the truth is that I don't know how to make a good one or even if we should do it.

Todo/ not todo

  • it looks like against expose/defineExpose design that findComponent(Component) to include exposed properties in the vm — even if $parent/$root chain access mentioned in the docs seems leave a door open for it.
  • At the same time, current VTU behaviour is inconsistent as per my reproduction demo. If findComponent(Component) includes exposed properties from a <script setup> component template direct import, it should also do it for it's explicitly bundled or runtime render function counterparts.

Anyways here's draft here, but I need help/guidance.

Cheers, have a nice weekend!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants