Skip to content

Commit

Permalink
Fully migrate to Vue 3
Browse files Browse the repository at this point in the history
The vue and vuex versions are pinned to what is currently provided by
MediaWiki core.

This patch needs some non-trivial changes to the jasmine tests:
In order to be able to mutate the inEditMode prop, we need to wrap the
component in a wrapper, because of vuejs/core#4874 not being fixed even
a year later. That in turn prevents us from calling the methods defined
on the component under test. Thus we need two functions to create new
widgets depending on which part of the interface we want to test,
methods or props.

Also, this no longer asserts the internal inEditMode value, because 1)
asserting internals is bad practice anyway, and 2) we no longer have
access to that in the wrapped component.

Bug: T304534
Change-Id: I82313a5eb6e8f19088de4a2e831666cdb656b1eb
Co-Authored-By: Michael Große <[email protected]>
  • Loading branch information
lucaswerkmeister and micgro42 committed Nov 3, 2022
1 parent d4175c8 commit 2a16c9e
Show file tree
Hide file tree
Showing 13 changed files with 489 additions and 106 deletions.
346 changes: 325 additions & 21 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@
"unexpected": "^10.32.1",
"unexpected-dom": "^5.0.1",
"unexpected-sinon": "^11.0.1",
"vue": "^2.6.11",
"vuex": "^3.1.3",
"vue": "3.2.37",
"vuex": "4.0.2",
"wdio-mediawiki": "^1.2.0",
"wdio-wikibase": "^5.2.0",
"webdriverio": "^7.16.14"
Expand Down
3 changes: 2 additions & 1 deletion resources/widgets/LexemeHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ wikibase.lexeme.widgets.buildLexemeHeader = ( function ( wb ) {

// make the app replace the existing #wb-lexeme-header (like in Vue 2) instead of appending to it (Vue 3 mount behavior)
var fragment = document.createDocumentFragment();
Vue.createMwApp( $.extend( { store: store }, header ) )
Vue.createApp( header )
.use( store )
.mount( fragment );
document.getElementById( 'wb-lexeme-header' ).replaceWith( fragment );
}
Expand Down
1 change: 0 additions & 1 deletion resources/widgets/LexemeHeader.newLexemeHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ module.exports = ( function () {
return function ( store, template, lemmaWidget, languageAndCategoryWidget, messages ) {
return {
template: template,
store: store,

data: function () {
return {
Expand Down
3 changes: 2 additions & 1 deletion resources/widgets/RepresentationWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ module.exports = ( function () {
* @return {Vue} Initialized widget
*/
function create( store, formIndex, element, template, beforeUpdate, mw ) {
return Vue.createMwApp( $.extend( { store: store }, newComponent( formIndex, template, beforeUpdate, mw ) ) )
return Vue.createApp( newComponent( formIndex, template, beforeUpdate, mw ) )
.use( store )
.mount( element );
}

Expand Down
32 changes: 27 additions & 5 deletions tests/jasmine/GlossWidget.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
describe( 'GlossWidget', function () {
var getTemplate = require('./helpers/template-loader');
var sinon = require( 'sinon' );

global.$ = require( 'jquery' ); // eslint-disable-line no-restricted-globals
global.mw = { // eslint-disable-line no-restricted-globals
Expand All @@ -17,7 +18,11 @@ describe( 'GlossWidget', function () {
}
},
message: function ( key ) {
return key;
return {
text: function() {
return key;
}
};
}
};

Expand All @@ -42,12 +47,26 @@ describe( 'GlossWidget', function () {

var expect = require( 'unexpected' ).clone();
expect.installPlugin( require( 'unexpected-dom' ) );
var Vue = global.Vue = require( 'vue/dist/vue.js' ); // eslint-disable-line no-restricted-globals
var GlossWidget = require( './../../resources/widgets/GlossWidget.js' );

var sandbox;
var mockLanguageSuggester = {
setSelectedValue: function () {},
};

beforeEach( function () {
sandbox = sinon.createSandbox();
} );

afterEach( function () {
sandbox.restore();
} );

it(
'create with no glosses - when switched to edit mode empty gloss is added',
function () {
$.fn.languagesuggester = sinon.stub(); // pretend the languagesuggester widget exists
sandbox.stub( $.prototype, 'data' ).returns( mockLanguageSuggester );
var widget = newWidget( [] );
var emptyGloss = { language: '', value: '' };

Expand All @@ -58,6 +77,8 @@ describe( 'GlossWidget', function () {
);

it( 'switch to edit mode', function ( done ) {
$.fn.languagesuggester = sinon.stub(); // pretend the languagesuggester widget exists
sandbox.stub( $.prototype, 'data' ).returns( mockLanguageSuggester );
var widget = newWidget( [ { language: 'en', value: 'gloss in english' } ] );

assertWidget( widget ).when( 'created' ).dom.hasNoInputFields();
Expand Down Expand Up @@ -95,6 +116,8 @@ describe( 'GlossWidget', function () {
} );

it( 'add a new gloss', function ( done ) {
$.fn.languagesuggester = sinon.stub(); // pretend the languagesuggester widget exists
sandbox.stub( $.prototype, 'data' ).returns( mockLanguageSuggester );
var widget = newWidget( [ { language: 'en', value: 'gloss in english' } ] );

assertWidget( widget ).when( 'created' ).dom.containsGloss(
Expand Down Expand Up @@ -221,8 +244,7 @@ describe( 'GlossWidget', function () {
},
getDirectionality
);
widget.el = document.createElement( 'div' );
widget.data = widget.data();
return new Vue( widget );
return Vue.createApp( widget )
.mount( document.createElement( 'div' ) );
}
} );
17 changes: 6 additions & 11 deletions tests/jasmine/ItemSelectorWrapper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,16 @@ describe( 'ItemSelectorWrapper', function () {
sandbox.stub( $.prototype, 'data' ).returns( mockEntitySelector )
$.fn.entityselector = sinon.stub(); // pretend the entityselector widget exists

component.$mount();
component.mount( document.createElement( 'div' ) );
} );

function newComponent( value ) {
var ItemSelectorWrapper = Vue.extend( newItemSelectorWrapper( { formatValue: function() {
return Vue.createApp(
newItemSelectorWrapper( { formatValue: function() {
return $.Deferred().resolve( { result: {} } );
}
}
) );

return new ItemSelectorWrapper( {
propsData: {
value: value
}
} );
} } ),
{ value: value }
);
}

} );
85 changes: 62 additions & 23 deletions tests/jasmine/LanguageAndLexicalCategoryWidget.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,30 @@ describe( 'LanguageAndLexicalCategoryWidget', function () {
expect.installPlugin( require( 'unexpected-dom' ) );

var newLanguageAndLexicalCategoryWidget = require( './../../resources/widgets/LanguageAndLexicalCategoryWidget.js' );
var reactiveRootProps;
var sinon = require( 'sinon' );
var sandbox;

beforeEach( function () {
sandbox = sinon.createSandbox();
} );

afterEach( function () {
sandbox.restore();
} );

it( 'shows the language and the lexical category', function () {
var mockEntitySelector = {
selectedEntity: function () {
},
destroy: sinon.stub()
};
sandbox.stub( $.prototype, 'data' ).returns( mockEntitySelector )
$.fn.entityselector = sinon.stub(); // pretend the entityselector widget exists

var language = 'Q123',
lexicalCategory = 'Q234',
widget = newWidget( language, lexicalCategory );
widget = newWidgetWithAccessibleMethods( language, lexicalCategory );

expect( widget.$el.textContent, 'to contain', 'Link for ' + language );
expect( widget.$el.textContent, 'to contain', 'Link for ' + lexicalCategory );
Expand All @@ -39,58 +50,86 @@ describe( 'LanguageAndLexicalCategoryWidget', function () {

it( 'switches to edit mode and back', async function () {
var mockEntitySelector = {
selectedEntity: function () {
},
selectedEntity: function () {},
destroy: sinon.stub()
};
sandbox.stub( $.prototype, 'data' ).returns( mockEntitySelector )
$.fn.entityselector = sinon.stub(); // pretend the entityselector widget exists

var widget = newWidget( 'Q123', 'Q234' );
var widget = newWidgetWithReactiveProps( 'Q123', 'Q234' );

expect( widget, 'not to be in edit mode' );

widget.inEditMode = true;
reactiveRootProps.inEditMode = true;
await widget.$nextTick();
expect( widget, 'to be in edit mode' );

widget.inEditMode = false;
reactiveRootProps.inEditMode = false;
await widget.$nextTick();
expect( widget, 'not to be in edit mode' );
} );

expect.addAssertion( '<object> [not] to be in edit mode', function ( expect, widget ) {
expect.errorMode = 'nested';

expect( widget.inEditMode, '[not] to be true' );
var no = expect.flags.not ? ' no ' : ' ';
expect( widget.$el, 'to contain' + no + 'elements matching', 'input' );
} );

function newWidget( language, lexicalCategory ) {
var template = getTemplate('resources/templates/languageAndLexicalCategoryWidget.vue.html');
function newWidgetWithAccessibleMethods( language, lexicalCategory ) {
var store = getStore( language, lexicalCategory );

return Vue.createApp(
getWidget(),
getProps( language, lexicalCategory )
).use( store ).mount( document.createElement( 'div' ) );
}

function newWidgetWithReactiveProps( language, lexicalCategory ) {
var store = getStore( language, lexicalCategory );

reactiveRootProps = Vue.reactive( getProps( language, lexicalCategory ) );
return Vue.createApp( {
render: function () {
return Vue.h(
getWidget(),
reactiveRootProps
);
},
} ).use( store ).mount( document.createElement( 'div' ) );
}

function getProps( language, lexicalCategory ) {
return {
language: language,
lexicalCategory: lexicalCategory,
inEditMode: false,
isSaving: false,
};
}

function getWidget() {
var template = getTemplate( 'resources/templates/languageAndLexicalCategoryWidget.vue.html' );
var mockApi = {
formatValue: function () { return Promise.resolve( { result: '' } ); },
formatValue: function () {
return Promise.resolve( { result: '' } );
},
};
var LanguageAndLexicalCategoryWidget = Vue.extend( newLanguageAndLexicalCategoryWidget( template, mockApi, {
return newLanguageAndLexicalCategoryWidget( template, mockApi, {
get: function ( key ) {
return key;
}
} ) );
} );
}

return new LanguageAndLexicalCategoryWidget( {
store: {
state: {
function getStore( language, lexicalCategory ) {
return Vuex.createStore( {
state: function () {
return {
languageLink: '<a href="#" class="language-link">Link for ' + language + '</a>',
lexicalCategoryLink: '<a href="#" class="lexical-category-link">Link for ' + lexicalCategory + '</a>'
}
},
propsData: {
language: language,
lexicalCategory: lexicalCategory,
inEditMode: false,
isSaving: false
};
}
} ).$mount();
} );
}
} );
Loading

0 comments on commit 2a16c9e

Please sign in to comment.