Page MenuHomePhabricator

Develop guidelines for migrating Vue test suites to V3
Closed, ResolvedPublic

Description

Several teams with Vue 2 codebases have been asking about how to migrate those tests to the current version of Vue (v3) and Vue Test Utils (v4).

Both @Jdforrester-WMF and @SimoneThisDot have been working on this (in Abstract Wikipedia and Structured Data MediaSearch, respectively), and I know that @Jdlrobson has also been looking into this.

The Design System Team should aggregate the lessons learned by various folks working on this issue and publish on-wiki documentation that we can point others to in the future.

We can post draft guidelines here for discussion until things are ready for publication on MediaWiki.

Draft Guidelines for Vue 3 Test suite migration

Useful references:
Example Migration patches
Software Dependencies
Vue

All Vue 2 apps which rely on the MediaWiki-provided copy of Vue are currently in the following situation: at runtime, they are using Vue 3.2 in compatibility mode, but all CLI testing is still relying on Vue 2 from NPM. The first step in migrating such a test suite is to use the correct version of Vue during testing.

Updates should be made in the following places:

packages.json: update Vue in devDependencies:

"vue": "3.2.23",
"@vue/compat": "3.2.23"

The test suite must then be configured to use Vue in compat mode.

Jest Config (jest.config.js): Configure the moduleNameMapper option:

moduleNameMapper: {
    "^vue$": "@vue/compat"
},

Jest Setup (jest.setup.js): Provide any necessary configuration to the compatibility build here (see https://v3-migration.vuejs.org/migration-build.html#global-config for more details)

const Vue = require( 'vue' );

Vue.configureCompat({
    // Add any necessary global config options here
    MODE: 3
} );
Other Test dependencies

In addition to Vue 3 and @vue/compat, the following dependencies should be updated in package.json devDependencies (source).

  • @vue/test-utils: Update to v2 (latest version is v 2.0.0-rc.17 at the time of writing)
  • @vue/compiler-sfc: This should match the Vue version, so 3.2.23 following the above
  • @vue/vue3-jest: The Vue Jest transformer (will be needed if you are using Jest). The version you need depends on the version of Jest you are using. If using Jest 26 and below, use vue-jest@5. If using Jest 27+, use @vue/vue3-jest. More details here: https://github.com/vuejs/vue-jest.
  • jest: You need to upgrade Jest to v27 if using @vue/vue3-jest.
  • @babel/preset-env may also need to be upgraded (MediaSearch is using ^7.16.11).
Jest Configuration

In addition to instructing Jest to use @vue/compat via the moduleNameMapper, the following settings should be specified in jest.config.js:

The transform property should point to whatever version of vue-jest is being used:

transform: {
    ".*\\.(vue)$": "<rootDir>/node_modules/@vue/vue3-jest"
},

Finally, the testEnvironment should be set to jsdom.

Component files
Exports vs Module.exports

In order to guarantee consistent behavior both in the browser (where ResourceLoader's CommonJS implementation is used) and in Node.js, we recommend doing the following in all .vue files:

// The following need to be changed to all .vue files
module.exports = exports = {
    // component code here...
}

See https://nodejs.org/docs/latest/api/modules.html#exports-shortcut for some information about why this is needed.

Plugins

TBD: Document changes to plugin API

Composition API

If you were previously using the Composition API plugin, this can be removed; all of the helpers you would use inside of setup() can now be require()-ed from Vue itself.

Emits option

In Vue 3, components can now declare what events they emit via an emits option. Doing this for all events a component can emit is strongly recommended. Without doing so, it is possible that component events whose names match those of native DOM events (like click) will get emitted twice, which could reasonably cause your tests to fail. More information here: https://v3-migration.vuejs.org/breaking-changes/emits-option.html. Relatedly, the .native event modifier has been removed.

TLDR; Add an emits option for all components which emit events: https://vuejs.org/api/options-state.html#emits

Attribute Coercion Behavior

In Vue 2, binding an attribute to a false-y value caused it to be removed from the rendered markup. In Vue 3, the attribute will show up with a value of attr="false" unless null or undefined is bound. More info: https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html

$children instance property removed

It is no longer possible to access the $children of a component instance. More info: https://v3-migration.vuejs.org/breaking-changes/children.html

Complete list of breaking changes

A complete description of breaking changes can be found here: https://v3-migration.vuejs.org/breaking-changes/. In most cases, it is possible to set a compatConfig flag on a per-component basis to keep the Vue 2 behavior until you are able to refactor the code in question; see the Vue 3 migration guide for more information about this.

Test suite changes

Some changes to your tests will also be necessary as part of the Vue Test Utils v1 -> v2 migration. In particular, propsData is now props, and createLocalVue no longer exists. Plugins and mocks can be added using the "global" property instead.

If you were only using createLocalVue to add the MediaWiki i18n plugin, you can now mock this globally in your jest.setup.js file. See: https://gerrit.wikimedia.org/r/c/mediawiki/extensions/WikiLambda/+/768139

// jest.setup.js
// Mock i18n for all tests
var vueTestUtils = require( '@vue/test-utils' );
vueTestUtils.config.global.mocks = {
    $i18n: jest.fn( function ( str ) {
    return str;
    } )
};

A full list of breaking changes in Vue Test Utils can be seen here: https://test-utils.vuejs.org/migration/#changes

Related in-progress test suite migrations

  • T302137: Vector test suite migration
  • T297722: MediaSearch test suite migration
  • T302134: MachineVision test suite migration
  • T302135: NearbyPages test suite migration
  • T302136: QuickSurvey's test suite migration
  • T302130: WikiLambda test suite migration
  • Wikibase migration?
  • Top-level task or all Vue test suite migration: T302129

Related Objects

Event Timeline

Dupe / super-task of T299588?

Thanks, I remembered seeing that but could not find it – I don't know who is paying attention to the DST "Requests" column (as opposed to the current sprint/kanban boards). I will merge that one here if that's okay.

Dupe / super-task of T299588?

Thanks, I remembered seeing that but could not find it – I don't know who is paying attention to the DST "Requests" column (as opposed to the current sprint/kanban boards). I will merge that one here if that's okay.

Sure!

egardner added subscribers: JKieserman, Catrope.

From T299588:

Our effective standard for writing Vue code is to use jest for local (non-MW) testing of Vue components. This will need some moderately significant changes to adapt for the migration to Vue3 and Vuex4; currently everything's still running tests on Vue2/Vuex3, despite production running Vue3 and as of this week Vue4.

Initial looking suggests:

Package changes
vue 2.6.11 -> 3.2.23 and added @vue/compat 3.2.23
vue-jest 3.0.5 -> @vue/vue3-jest 27.0.0-alpha.4
@vue/test-utils 1.2.0 -> 2.0.0-rc.18
eslint-plugin-jest 24.3.3 -> 25.7.0
jest 26.0.1 -> 27.4.7
vuex 3.6.2 -> 4.0.2

jest config changes
???

Here are Some of the changes we made in the MediaSearch Vue upgrade.

Jest config - jest.config.js - from this patch https://gerrit.wikimedia.org/r/c/mediawiki/extensions/MediaSearch/+/757422/2/jest.config.js

  • Add @Vue/compat as part of the moduleNameMapper:
moduleNameMapper: {
    "^vue$": "@vue/compat"
},
  • Set test environment (otherwise $ will fail to load in global context
testEnvironment: "jsdom",
  • Upgrade the .vue files transformer:
transform: {
    ".*\\.(vue)$": "<rootDir>/node_modules/@vue/vue3-jest"
},

Jest setup - jest.setup.js - from this patch https://gerrit.wikimedia.org/r/c/mediawiki/extensions/MediaSearch/+/757422/2/jest.setup.js

  • Set Vue to use vue 3 mode
const Vue = require( 'vue' );

Vue.configureCompat( {
	MODE: 3
} );

package.json - from this file https://gerrit.wikimedia.org/r/c/mediawiki/extensions/MediaSearch/+/757422/2/package.json

"@babel/preset-env": "^7.16.11",
"@vue/compat": "3.2.23",
"@vue/compiler-sfc": "3.2.23",
"@vue/test-utils": "2.0.0-rc.18",
"@vue/vue3-jest": "27.0.0-alpha.4",
"jest": "^27.0.0",
"jest-when": "^3.2.1",
"vue": "3.2.23",
"vuex": "4.0.2",

Actual Vue file changes needed

  • Define export twice ( I do not remember why we had to do this, but it is needed for test to pass. Happy to change it to a better solution if you find it)
// The following need to be changed to all .vue files
module.exports = exports = {
}
  • Native Event will emit twice
The following code will trigger 2 emit messages in Vue 3 as the native click event will be triggered together with the custom one. 
<button @click="$emit('click')" />

The solution is to "remove" the custom click event, as the button will trigger a click anyway:
<button />

Changes to Tests files .test.js

  • No need to specify localVue anymore into test files. plugins and Mocks are added using the "global" property:
//Before
const localVue = VueTestUtils.createLocalVue();
localVue.use( i18n );

// After
VueTestUtils.config.global.plugins = [ i18n ];
  • Mocks and plugin (for component instances) are now part of the global object:
// before
VueTestUtils.shallowMount( Component, {
    store: store,
    localVue: localVue,
    mocks: {
        $log: jest.fn()
    }
}
// After

VueTestUtils.shallowMount( Component, {
    global: {
        plugins: [ store ]
        mocks: {
            $log: jest.fn()
        }
    }
}
  • PropsData has been renamed to props:
// before 
const wrapper = VueTestUtils.mount( QuickView, {
    propsData: {
        title: "title"
    }
} );
// After 
const wrapper = VueTestUtils.mount( QuickView, {
    props: {
        title: "title"
    }
} );
// Inside our Plugin file
// Before
module.exports = {
  install: function ( Vue ) {
    Vue.prototype.$i18n = function ( message ) {
      return {
        text: jest.fn().mockReturnValue( message )
      };
    };
}

// after
module.exports = {
  install: function ( Vue ) {
    Vue.config.globalProperties.$i18n = function ( message ) {
      return {
        text: jest.fn().mockReturnValue( message )
      };
    };
}
  • Native Event will emit twice
The following code will trigger 2 emit messages in Vue 3 as the native click event will be triggered together with the custom one. 
<button @click="$emit('click')" />

The solution is to "remove" the custom click event, as the button will trigger a click anyway:
<button />

I would recommend instead fixing this by explicitly declaring 'click' as an emitted event in the component options:

"emits": [
    "click"
]
egardner updated the task description. (Show Details)
egardner updated the task description. (Show Details)

Change 755499 had a related patch set uploaded (by Jforrester; author: Jforrester):

[mediawiki/extensions/WikiLambda@master] build: Upgrade local Jest Vue/Vuex testing code to work with Vue 3

https://gerrit.wikimedia.org/r/755499

Change 755499 merged by jenkins-bot:

[mediawiki/extensions/WikiLambda@master] build: Upgrade local Jest Vue/Vuex testing code to work with Vue 3

https://gerrit.wikimedia.org/r/755499

egardner updated the task description. (Show Details)

I've tried a few more (Nearby, Vector, RelatedArticles and QuickSurveys - see T300573 T286835 ) and have a few additions on top of T302831#7745924
I touched parcel Js, jsdoc and storybook so associated patches dare likely helpful.

jsdoc-vue@1.0.0 =>jsdoc-vuejs@4.0.0
jsdoc@3.6.7 => jsdoc@3.6.10

I don't think anyone else is using parcel bundler.

For local components I had to use the following patterns to support storybook, jest and Resourceloader:

App = require( './App.vue' ).default || require( './App.vue' );
components: {
  pagelist: PageList.name ? PageList : PageList.default,
}
Define export twice ( I do not remember why we had to do this, but it is needed for test to pass. Happy to change it to a better solution if you find it)

Really curious about the reasoning for this one, as I did need this in all 4 ports.

I think we can declare this Resolved now?

Marking this as resolved since there doesn't seem to be any outstanding follow-ups. If anything new pops up, please add it to the "Needs Triage" column on our new Phab board - thanks!