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

Usage of VueProxy with functions expecting Vue type #125

Closed
IlCallo opened this issue Sep 9, 2019 · 5 comments
Closed

Usage of VueProxy with functions expecting Vue type #125

IlCallo opened this issue Sep 9, 2019 · 5 comments

Comments

@IlCallo
Copy link

IlCallo commented Sep 9, 2019

Hi, thank you for the great work!

I have some functions (wrappers for Jest mount() helper) which expect a Vue parameter to provide autocomplete for the vm.

Until ac3581b, autocomplete didn't worked but at least it didn't fire off any error because the component inerithed from VueConstructor<never> and was recognized as a Vue instance.

Now I get an error because a lot of Vue properties are missing from VueProxy (rightfully).

I tryed to find a way to make the function work both with Vue and VueProxy (because as a matter of fact I don't really need all the stuff on Vue type, VueProxy should be enough), but it seems there is no VueClass equivalent which manages VueProxy.

Also, VueProxy itself is not exported and, even if it was, I would have to manually specify PropsOptions and RawBindings instead of having them inferred from the component.

Any hint on how to proceed?
For the time being I just reverted VueConstructorProxy typings in my local setup, but I'd like to solve the problem and make everything work properly.

This issue is related to my previous comment here and have the same root cause.
Official mount() and shallowMount() testing helpers don't have autocomplete because of the VueProxy usage.

Maybe an helper can be provided which converts back from VueProxy to a Vue instance?

Bonus: if there is some way to extract props typings from VueProxy, I'm interested in that as well.

My factory generator functions
import VueCompositionApi from '@vue/composition-api';
import { createLocalVue, shallowMount, VueClass } from '@vue/test-utils';
import { Cookies, Quasar, QuasarPluginOptionsExt } from 'quasar';
import Vue from 'vue';

const mockSsrContext = () => {
  return {
    req: {
      headers: {},
    },
    res: {
      setHeader: () => undefined,
    },
  };
};

export const mountQuasar = <V extends Vue>(
  component: VueClass<V>,
  options: {
    quasar?: Partial<QuasarPluginOptionsExt>;
    ssr?: boolean;
    cookies?: any;
    plugins?: any;
    propsData?: any;
  } = {},
) => {
  const localVue = createLocalVue();
  const app = {};

  localVue.use(Quasar, options.quasar);
  localVue.use(VueCompositionApi);

  if (options) {
    const ssrContext = options.ssr ? mockSsrContext() : null;

    if (options.cookies) {
      const cookieStorage = ssrContext ? Cookies.parseSSR(ssrContext) : Cookies;
      const cookies = options.cookies;
      Object.keys(cookies).forEach(key => {
        cookieStorage.set(key, cookies[key]);
      });
    }

    if (options.plugins) {
      options.plugins.forEach((plugin: any) => {
        plugin({
          app,
          Vue: localVue,
          ssrContext,
        });
      });
    }
  }

  // mock vue-i18n
  const $t = () => {};
  const $tc = () => {};
  const $n = () => {};
  const $d = () => {};

  // Works both with Vue file and TS file because of some underlying black magic
  // See https://github.com/vuejs/vue-jest/issues/188
  return shallowMount(component, {
    propsData: options.propsData,
    localVue,
    mocks: { $t, $tc, $n, $d },
    // Injections for Components with a QPage root Element
    provide: {
      pageContainer: true,
      layout: {
        header: {},
        right: {},
        footer: {},
        left: {},
      },
    },
  });
};

export function mountFactory<V extends Vue>(
  component: VueClass<V>,
  options: {
    quasar?: Partial<QuasarPluginOptionsExt>;
    ssr?: boolean;
    cookies?: any;
    plugins?: any;
  } = {},
) {
  // TODO: add prop typings based on component
  return (propsData?: any) => mountQuasar(component, { ...options, propsData });
}
Usage
import { QBtn, QIcon, QItem, QItemSection, QList } from 'quasar';
import { mountFactory } from 'test/jest/utils';
import MyComponent from './my-component';

const factory = mountFactory(MyComponent, { // Throws error because MyComponent is not of type Vue
  quasar: {
    components: {
      QBtn,
      QItemSection,
      QItem,
      QIcon,
      QList,
    },
  },
});

describe('MyComponent', () => {
  it('is a Vue instance', () => {
    const wrapper = factory({ propName: 'propValue' });

    console.log(wrapper.vm.propName); // Even with previous typings, autocomplete for this wouldn't have worked

    expect(wrapper.isVueInstance()).toBeTruthy();
  });
});
@liximomo
Copy link
Member

Fixed in 0.3.2

@IlCallo
Copy link
Author

IlCallo commented Oct 1, 2019

@liximomo I upgraded to 0.3.2.
Hard compilations errors are solved now, tweaking a bit my factories generation code by using ComponentOptions<V> instead of VueClass<V>.

Yet, mounting a component like shallowMount(MyComponent) returns a Wrapper<Vue> instead of Wrapper<MyComponent>, which breaks autocomplete capabilities.

I attach my updated code for reference, but the problem is at shallowMount level, independent from my code scope.

My factory generator functions
import VueCompositionApi from '@vue/composition-api';
import { createLocalVue, shallowMount, VueClass } from '@vue/test-utils';
import { Cookies, Quasar, QuasarPluginOptionsExt } from 'quasar';
import Vue from 'vue';

const mockSsrContext = () => {
  return {
    req: {
      headers: {},
    },
    res: {
      setHeader: () => undefined,
    },
  };
};

// We cannot infer component type from `shallowMount` using `Parameters<typeof shallowMount>`
//  because it has overloads but the last signature isn't the most general one, while `Parameters<...>`
//  will automatically resolve to the last signature thinking it's the most generic one.
// See https://github.com/Microsoft/TypeScript/issues/24275#issuecomment-390701982
export const mountQuasar = <V extends Vue>(
  component: ComponentOptions<V>,
  options: {
    quasar?: Partial<QuasarPluginOptionsExt>;
    ssr?: boolean;
    cookies?: any;
    plugins?: any;
    propsData?: any;
  } = {},
) => {
  const localVue = createLocalVue();
  const app = {};

  localVue.use(Quasar, options.quasar);
  localVue.use(VueCompositionApi);

  if (options) {
    const ssrContext = options.ssr ? mockSsrContext() : null;

    if (options.cookies) {
      const cookieStorage = ssrContext ? Cookies.parseSSR(ssrContext) : Cookies;
      const cookies = options.cookies;
      Object.keys(cookies).forEach(key => {
        cookieStorage.set(key, cookies[key]);
      });
    }

    if (options.plugins) {
      options.plugins.forEach((plugin: any) => {
        plugin({
          app,
          Vue: localVue,
          ssrContext,
        });
      });
    }
  }

  // mock vue-i18n
  const $t = () => {};
  const $tc = () => {};
  const $n = () => {};
  const $d = () => {};

  // Works both with Vue file and TS file because of some underlying black magic
  // See https://github.com/vuejs/vue-jest/issues/188
  return shallowMount(component, {
    propsData: options.propsData,
    localVue,
    mocks: { $t, $tc, $n, $d },
    // Injections for Components with a QPage root Element
    provide: {
      pageContainer: true,
      layout: {
        header: {},
        right: {},
        footer: {},
        left: {},
      },
    },
  });
};

export function mountFactory(
  ...[component, options]: Parameters<typeof mountQuasar>
) {
  // TODO: add prop typings based on component
  return (propsData?: any) => mountQuasar(component, { ...options, propsData });
}
Usage
import { QBtn, QIcon, QItem, QItemSection, QList } from 'quasar';
import { mountFactory } from 'test/jest/utils';
import MyComponent from './my-component';

const factory = mountFactory(MyComponent, { // Doesn't throw error because it recognize MyComponent as a ComponentOptions<V> instance
  quasar: {
    components: {
      QBtn,
      QItemSection,
      QItem,
      QIcon,
      QList,
    },
  },
});

describe('MyComponent', () => {
  it('is a Vue instance', () => {
    const wrapper = factory({ propName: 'propValue' });

    console.log(wrapper.vm.propName); // Component typings still doesn't work

    expect(wrapper.isVueInstance()).toBeTruthy();
  });
});

@IlCallo
Copy link
Author

IlCallo commented Jul 24, 2020

@pikax hey there, I came back to this issue after some months as I'm trying to finish up the work for Quasar Framework jest App Extension, but the same problem as my last post persists: vue-test-utils mount functions aren't able to infer the underlying Vue instance because VueProxy doesn't expose it.

Is there a possibility to get this working or shall I just accept there is no way to fix this and hope in Vue 3 better TS support?

@pikax
Copy link
Member

pikax commented Jul 24, 2020

Hey @IlCallo,

Can you open a new issue with a reproduction code, a lot of things changed since 0.3.2, I'm currently doing some v3 stuff so I will most likely mix information if I have a proper look now.

Create a new issue to keep track otherwise this will get lost.

There's plans to improve typings on 2.7.x vuejs/vue#11488 but is still on hold, because if that goes forward it will add a lot of breaking changes.

@IlCallo
Copy link
Author

IlCallo commented Jul 24, 2020

Checked again, resolved by changing my function types overloads order (VueClass version must be before ComponentOptions one).
Works as expected, sorry for bothering

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants