vue.jsunit-testingvuejs2vue-composition-apivue-test-utils

Vue 2 + Composition API + vue-test-utils 1.3.6: `Cannot redefine property` and `setProps` doesn’t trigger `watchers`


Problem:

I’m migrating a Vue 2.6 app to the Composition API (via the plugin) and hit two testing issues with vue-test-utils (v1) + Jest:

  1. Jest couldn’t mock functions from a plain TS module (not a Vue component). Error: Cannot redefine property: ...

  2. wrapper.setProps() did not trigger Composition API watchers, so my side-effects wouldn’t run in tests.


Environment


Solution

  • Here is what actually worked for me:

    1) Module mocks fail with Cannot redefine property

    Trying to jest.mock() a plain TS helper module caused Cannot redefine property.

    Cause

    When mocking ES modules / TS transpiled output, Jest needs the __esModule flag to handle the default/named export shape correctly. Without it, spreading the actual module or re-defining its properties can blow up.

    Fix

    Mark the mock as an ES module and spread the real implementation:

    import * as utils from '@/path/to/your/component';
    
    jest.mock('@/path/to/your/component', () => {
      return {
        __esModule: true, // <-- important
        ...jest.requireActual('@/path/to/your/component'),
      };
    });
    

    Then you can spy as usual:

    const updateGeoJSONSpy = jest.spyOn(utils, 'updateGeoJSON');
    

    Original answer https://stackoverflow.com/a/72885576/1216480


    2) setProps() doesn’t trigger Composition API watch in Vue 2 tests

    A watch inside setup() that depends on a prop didn’t run when I used wrapper.setProps(...).

    Cause

    With Vue 2 + Composition API plugin + vue-test-utils 1.3.6, prop updates via setProps don’t reliably re-trigger watchers created inside setup(). It’s a known limitation/quirk of the legacy stack.

    Fix (workaround)

    Use wrapper.setData(...) to update the reactive source that your watch depends on. This updates component state without remounting and does trigger the watcher. (Note: onMounted won’t run again, this is expected.)

    const updateGeoJSONSpy = jest.spyOn(utils, 'updateGeoJSON');
    
    const geojson = {
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [10, 50] },
    };
    
    const wrapper = mount(MapDisplay, {
      propsData: {
        mapData: { geojson },
      },
    });
    
    await wrapper.vm.$nextTick();
    
    // initial call
    expect(updateGeoJSONSpy).toHaveBeenCalled();
    
    // mutate reactive data to trigger watch
    await wrapper.setData({
      mapData: {
        geojson: {
          type: 'Feature',
          geometry: { type: 'Point', coordinates: [12, 15] },
        },
      },
    });
    
    expect(updateGeoJSONSpy).toHaveBeenCalledTimes(2);
    

    Notes:


    Minimal component sketch (for context)

    // MapDisplay.vue (sketch)
    import Vue from 'vue';
    import { defineComponent, watch } from '@vue/composition-api';
    import { updateGeoJSON } from '@/components/WoIstMeinWald/utils';
    
    export default defineComponent({
      name: 'MapDisplay',
      props: {
        mapData: { type: Object, required: true },
      },
      setup(props) {
        watch(
          () => props.mapData?.geojson,
          (val) => {
            if (val) updateGeoJSON(val);
          },
          { immediate: true }
        );
    
        return {};
      },
    });
    

    Takeaways