jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/362180 )

Change subject: Multi-variant form representations.
......................................................................


Multi-variant form representations.

Integrates the RepresentationsWidget into the formview.

Bug: T165575
Change-Id: I5dda249fb2121db3b29804e202b9f90d3755a968
---
M extension.json
M resources/jquery.wikibase.lexemeformview.js
M resources/lexeme.css
M resources/templates.php
A resources/widgets/RepresentationWidget.js
M src/View/LexemeFormsView.php
M src/WikibaseLexeme.hooks.php
M tests/browser/features/forms.feature
M tests/browser/features/step_definitions/forms_steps.rb
M tests/browser/features/support/pages/lexeme_page.rb
M tests/phpunit/mediawiki/View/LexemeFormsViewTest.php
M tests/qunit/jquery.wikibase.lexemeformview.tests.js
A tests/qunit/widgets/RepresentationWidget.tests.js
13 files changed, 669 insertions(+), 117 deletions(-)

Approvals:
  Jonas Kress (WMDE): Looks good to me, approved
  jenkins-bot: Verified



diff --git a/extension.json b/extension.json
index 90cf4a5..d68d699 100644
--- a/extension.json
+++ b/extension.json
@@ -78,7 +78,8 @@
                        "dependencies": [
                                "jquery.ui.EditableTemplatedWidget",
                                "wikibase.templates.lexeme",
-                               "jquery.wikibase.grammaticalfeatureview"
+                               "jquery.wikibase.grammaticalfeatureview",
+                               "wikibase.lexeme.widgets.RepresentationWidget"
                        ],
                        "messages": [
                                "wikibase-lexeme-empty-form-representation",
@@ -144,6 +145,16 @@
                                "wikibase-remove"
                        ]
                },
+               "wikibase.lexeme.widgets.RepresentationWidget": {
+                       "scripts": "widgets/RepresentationWidget.js",
+                       "dependencies": [
+                               "vue"
+                       ],
+                       "messages":[
+                               "wikibase-add",
+                               "wikibase-remove"
+                       ]
+               },
                "wikibase.lexeme.widgets.GlossWidget": {
                        "scripts": "widgets/GlossWidget.js",
                        "dependencies": [
diff --git a/resources/jquery.wikibase.lexemeformview.js 
b/resources/jquery.wikibase.lexemeformview.js
index e0c3108..6299ab8 100644
--- a/resources/jquery.wikibase.lexemeformview.js
+++ b/resources/jquery.wikibase.lexemeformview.js
@@ -3,6 +3,9 @@
 
        var PARENT = $.ui.EditableTemplatedWidget;
 
+       /** @type {wikibase.lexeme.widgets.RepresentationWidget} */
+       var RepresentationWidget = require( 
'wikibase.lexeme.widgets.RepresentationWidget' );
+
        /** @type {wikibase.datamodel.TermMap}*/
        var TermMap = wb.datamodel.TermMap;
        /** @type {wikibase.datamodel.Term}*/
@@ -34,19 +37,19 @@
                        template: 'wikibase-lexeme-form',
                        templateParams: [
                                function () {
-                                       return 'some lang';
+                                       var $container = $( '<span/>' );
+                                       this.deferredFormWithId.promise().then( 
function ( form ) {
+                                               $container.text( form.getId() );
+                                       } );
+
+                                       return $container;
                                },
                                '',
-                               function () {
-                                       return '';
-                               },
                                function () {
                                        return mw.wbTemplate( 
'wikibase-lexeme-form-grammatical-features', '' );
                                },
                                function () {
                                        var $container = $( '<div/>' );
-                                       this.deferredFormWithId = $.Deferred();
-
                                        this.deferredFormWithId.promise().then( 
function ( form ) {
                                                var $header = $( '<h2/>' 
).applyTemplate(
                                                        'wb-section-heading',
@@ -70,9 +73,9 @@
                                }
                        ],
                        templateShortCuts: {
-                               $text: '.wikibase-lexeme-form-text',
                                $id: '.wikibase-lexeme-form-id',
-                               $grammaticalFeatures: 
'.wikibase-lexeme-form-grammatical-features'
+                               $grammaticalFeatures: 
'.wikibase-lexeme-form-grammatical-features',
+                               $representations: '.form-representations'
                        },
                        inputNodeName: 'TEXTAREA',
                        api: null,
@@ -86,6 +89,8 @@
 
                _grammaticalFeatureView: null,
 
+               _representationsWidget: null,
+
                /**
                 * This method acts as a setter if it is given a LexemeForm 
object.
                 * Otherwise it returns its value if it is not in edit mode and 
returns a new LexemeForm from its
@@ -98,11 +103,9 @@
                        if ( form instanceof 
wikibase.lexeme.datamodel.LexemeForm ) {
                                this.option( 'value', form );
                                this._grammaticalFeatureView.value( 
form.getGrammaticalFeatures() );
-                               if ( this.deferredFormWithId ) {
-                                       if ( form.getId() ) {
-                                               
this.deferredFormWithId.resolve( form );
-                                               this.deferredFormWithId = null;
-                                       }
+                               if ( this.deferredFormWithId && form.getId() ) {
+                                       this.deferredFormWithId.resolve( form );
+                                       this.deferredFormWithId = null;
                                }
                                this.draw();
                                return;
@@ -112,21 +115,16 @@
                                return this.options.value;
                        }
 
-                       var representations = new TermMap( {
-                               en: new Term(
-                                       'en',
-                                       this.$text.children( this.inputNodeName 
).val()
-                               )
-                       } );
-
                        return new wikibase.lexeme.datamodel.LexemeForm(
                                this.options.value ? this.options.value.getId() 
: null,
-                               representations,
+                               arrayToTermMap( 
this._representationsWidget.representations ),
                                this._grammaticalFeatureView ? 
this._grammaticalFeatureView.value() : []
                        );
                },
 
                _create: function () {
+                       this.deferredFormWithId = $.Deferred();
+
                        PARENT.prototype._create.call( this );
 
                        this._grammaticalFeatureView = 
this._buildGrammaticalFeatureView();
@@ -134,6 +132,8 @@
                                this.value(),
                                $( '.wikibase-statementgrouplistview', 
this.element )
                        );
+
+                       this._buildRepresentations( this.value() );
                },
 
                _buildGrammaticalFeatureView: function 
buildGrammaticalFeatureView() {
@@ -147,8 +147,6 @@
                                api: self.options.api
                        } );
 
-                       //FIXME add representation change propagation
-
                        this.$grammaticalFeatures.on( 
'grammaticalfeatureviewchange', function () {
                                self._trigger( 'change' );
                        } );
@@ -158,22 +156,37 @@
 
                _startEditing: function () {
                        this._inEditMode = true;
-                       this._grammaticalFeatureView.startEditing();// FIXME 
this line breaks edit mode when adding lexeme form
+                       this._grammaticalFeatureView.startEditing();
+                       this._representationsWidget.edit();
                        return this.draw();
                },
 
                _stopEditing: function ( dropValue ) {
                        this._inEditMode = false;
-                       if ( dropValue && 
this.options.value.getRepresentation() === '' ) {
-                               this.$text.children( this.inputNodeName ).val( 
'' );
+                       if ( dropValue ) {
+                               this._representationsWidget.representations = 
termMapToArray( this.value().getRepresentations() );
                        }
                        this._grammaticalFeatureView.stopEditing( dropValue );
+                       this._representationsWidget.stopEditing();
 
                        return this.draw();
                },
 
                isInEditMode: function () {
                        return this._inEditMode;
+               },
+
+               _buildRepresentations: function ( form ) {
+                       var representations = form ? termMapToArray( 
form.getRepresentations() ) : [];
+
+                       this._representationsWidget = 
RepresentationWidget.create(
+                               representations,
+                               this.$representations[ 0 ],
+                               '#representation-widget-vue-template',
+                               function () {
+                                       this._trigger( 'change' );
+                               }.bind( this )
+                       );
                },
 
                /**
@@ -187,7 +200,6 @@
                        }
 
                        if ( !this.isInEditMode() && !value ) {
-                               this.$text.text( mw.msg( 
'wikibase-lexeme-empty-form-representation' ) );
                                // Apply lang and dir of UI language
                                // instead language of that row
                                var userLanguage = mw.config.get( 
'wgUserLanguage' );
@@ -197,30 +209,38 @@
                                return deferred.resolve().promise();
                        }
 
-                       if ( !this.isInEditMode() ) {
-                               this.$text.text( 
value.getRepresentations().getItemByKey( 'en' ).getText() );
-                               this.$id.text( ' (' + value.getId() + ')' ); // 
TODO: whitespace and brackets (?) should be i18nable
-
-                               return deferred.resolve().promise();
-                       }
-
-                       var $input = $( document.createElement( 
this.options.inputNodeName ) )
-                               .attr( 'placeholder', mw.msg( 
'wikibase-lexeme-enter-form-representation' ) )
-                               .on( 'change', function () { this._trigger( 
'change' ); }.bind( this ) );
-
-                       if ( value ) {
-                               $input.val( 
value.getRepresentations().getItemByKey( 'en' ).getText() );
-                       }
-
-                       if ( $.fn.inputautoexpand ) {
-                               $input.inputautoexpand( {
-                                       suppressNewLine: true
-                               } );
-                       }
-
-                       this.$text.empty().append( $input );
-
                        return deferred.resolve().promise();
                }
        } );
+
+       function arrayToTermMap( representations ) {
+               var result = new wikibase.datamodel.TermMap();
+
+               representations.forEach( function ( representation ) {
+                       try {
+                               result.setItem(
+                                       representation.language,
+                                       new wikibase.datamodel.Term( 
representation.language, representation.value )
+                               );
+                       } catch ( e ) {
+                               // ignore
+                       }
+               } );
+
+               return result;
+       }
+
+       /**
+        * @param {wikibase.datamodel.TermMap} representations
+        * @return {Array}
+        */
+       function termMapToArray( representations ) {
+               var result = [];
+
+               representations.each( function ( language, term ) {
+                       result.push( { language: term.getLanguageCode(), value: 
term.getText() } );
+               } );
+
+               return result;
+       }
 }( jQuery, mediaWiki, wikibase ) );
diff --git a/resources/lexeme.css b/resources/lexeme.css
index 6d6fb72..d0bad3d 100644
--- a/resources/lexeme.css
+++ b/resources/lexeme.css
@@ -18,6 +18,14 @@
        clear: left;
 }
 
+.wikibase-lexeme-form-header {
+       display: flex;
+       align-items: center;
+}
+.wikibase-lexeme-form-id {
+       font-size: 2em;
+}
+
 .wikibase-lexeme-forms .wikibase-lexeme-form-representation {
        clear: left;
        margin-left: 10px; /* same as .wb-section-heading */
@@ -35,10 +43,6 @@
        font-size: 0.9em;
        height: 1.5em;
        vertical-align: middle;
-}
-
-.wikibase-lexeme-form-representation .wikibase-lexeme-form-id {
-       color: #72777d;
 }
 
 .wikibase-lexeme-forms-section > .wikibase-toolbar-container {
@@ -174,6 +178,116 @@
        cursor: pointer;
 }
 
+.representation-widget {
+       display: flex;
+       margin: 0 2em;
+}
+
+.representation-widget_representation-list {
+       margin: 0 !important;
+       padding: 0 !important;
+       list-style: none;
+       display: flex;
+       flex-wrap: wrap;
+       flex-grow: 1;
+}
+
+.representation-widget_representation {
+       display: flex;
+       flex-direction: column;
+       border-left: 6px solid #ededed;
+       padding: 0 9px;
+       margin: 11px 0;
+}
+
+.representation-widget_representation-value {
+       font-size: 2em;
+       font-weight: bold;
+}
+
+.representation-widget_representation-language {
+       font-size: 1em;
+}
+
+.representation-widget_edit-area {
+       display: flex;
+       border: 2px solid #99ccff;
+}
+
+.representation-widget_controls {
+       width: 100px;
+       padding: 20px 40px;
+       background: #dae8fc;
+       display: flex;
+       flex-direction: column;
+       justify-content: center;
+}
+
+.representation-widget_control {
+       background: transparent;
+       border: none;
+       font-size: 1em;
+       margin: 10px;
+       cursor: pointer;
+       color: #0d4c99;
+}
+
+.representation-widget_representation-edit-box {
+       position: relative;
+       display: flex;
+       flex-direction: column;
+       justify-content: space-around;
+       border-left: 6px solid #dae8fc;
+       padding: 0 45px 0 9px;
+       margin: 11px 0;
+
+       height: 132px;
+}
+
+.representation-widget_representation-value-input {
+       resize: horizontal;
+       min-width: 120px;
+       padding-right: 20px;
+
+       font-size: 2em;
+}
+
+.representation-widget_representation-value-input:active {
+       width: auto;
+}
+
+.representation-widget_representation-language-input {
+       resize: horizontal;
+       min-width: 120px;
+       font-size: 1em;
+       padding-right: 20px;
+}
+
+.representation-widget_representation-language-input:active {
+       width: auto;
+}
+
+.representation-widget_representation-remove {
+       border: none;
+       background: transparent;
+       color: #074c99;
+       font-size: 2em;
+       cursor: pointer;
+
+       position: absolute;
+       top: -11px;
+       right: 23px;
+}
+
+.representation-widget_add {
+       height: 100%;
+       border: none;
+       font-size: 2em;
+       text-align: center;
+       background: #dae8fc;
+       cursor: pointer;
+}
+
 .wikibase-lexeme-senses .wikibase-lexeme-sense {
        position: relative;
        clear: left;
@@ -254,4 +368,4 @@
 
 .wikibase-lexeme-sense > .wikibase-edittoolbar-container {
        display: none; /* FIXME: temporarily hidden until entering/editing 
glosses is possible */
-}
\ No newline at end of file
+}
diff --git a/resources/templates.php b/resources/templates.php
index 885d64d..b3d9136 100644
--- a/resources/templates.php
+++ b/resources/templates.php
@@ -16,12 +16,12 @@
 
        $templates['wikibase-lexeme-form'] = <<<'HTML'
 <div class="wikibase-lexeme-form">
-       <h3 class="wikibase-lexeme-form-representation" lang="$1">
-               <span class="wikibase-lexeme-form-text">$2</span>
-               <span class="wikibase-lexeme-form-id wikibase-title-id"> 
$3</span>
-       </h3>
+       <div class="wikibase-lexeme-form-header">
+               <div class="wikibase-lexeme-form-id">$1</div>
+               <div class="form-representations">$2</div>
+       </div>
+       $3
        $4
-       $5
 </div>
 HTML;
 
diff --git a/resources/widgets/RepresentationWidget.js 
b/resources/widgets/RepresentationWidget.js
new file mode 100644
index 0000000..208f372
--- /dev/null
+++ b/resources/widgets/RepresentationWidget.js
@@ -0,0 +1,78 @@
+module.exports = ( function ( mw ) {
+       'use strict';
+
+       /**
+        * @callback wikibase.lexeme.widgets.RepresentationWidget.newComponent
+        *
+        * @param {{language:string, value: string}[]} representations
+        * @param {string|HTMLElement} element - ID selector or DOM node
+        * @param {string} template - template string or ID selector
+        * @param {function} beforeUpdate
+        *
+        * @return {object} Vue component object
+        */
+       function newComponent( representations, element, template, beforeUpdate 
) {
+               return {
+                       el: element,
+                       template: template,
+
+                       beforeUpdate: beforeUpdate,
+
+                       data: {
+                               inEditMode: false,
+                               representations: representations
+                       },
+                       methods: {
+                               edit: function () {
+                                       this.inEditMode = true;
+                                       if ( this.representations.length === 0 
) {
+                                               this.add();
+                                       }
+                               },
+                               stopEditing: function () {
+                                       this.inEditMode = false;
+                               },
+                               add: function () {
+                                       if ( !this.inEditMode ) {
+                                               throw new Error( 'Cannot add 
representation if not in edit mode' );
+                                       }
+                                       this.representations.push( { language: 
'', value: '' } );
+                               },
+                               remove: function ( representation ) {
+                                       if ( !this.inEditMode ) {
+                                               throw new Error( 'Cannot remove 
representation if not in edit mode' );
+                                       }
+                                       var index = 
this.representations.indexOf( representation );
+                                       this.representations.splice( index, 1 );
+                               }
+                       },
+                       filters: {
+                               message: function ( key ) {
+                                       return mw.messages.get( key );
+                               }
+                       }
+               };
+       }
+
+       /**
+        * @callback wikibase.lexeme.widgets.RepresentationWidget.create
+        *
+        * @param {{language: string, value: string}[]} representations
+        * @param {string|HTMLElement} element - ID selector or DOM node
+        * @param {string} template - template string or ID selector
+        * @param {function} beforeUpdate
+        *
+        * @return {Vue} Initialized widget
+        */
+       function create( representations, element, template, beforeUpdate ) {
+               return new Vue( newComponent( representations, element, 
template, beforeUpdate ) );
+       }
+
+       /**
+        * @class wikibase.lexeme.widgets.RepresentationWidget
+        */
+       return {
+               create: create
+       };
+
+} )( mediaWiki );
diff --git a/src/View/LexemeFormsView.php b/src/View/LexemeFormsView.php
index fe45c97..259e7bf 100644
--- a/src/View/LexemeFormsView.php
+++ b/src/View/LexemeFormsView.php
@@ -3,11 +3,13 @@
 namespace Wikibase\Lexeme\View;
 
 use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\DataModel\Term\Term;
 use Wikibase\Lexeme\DataModel\Form;
 use Wikibase\Lexeme\View\Template\LexemeTemplateFactory;
 use Wikibase\Lib\EntityIdHtmlLinkFormatter;
 use Wikibase\View\LocalizedTextProvider;
 use Wikibase\View\StatementSectionsView;
+use WMDE\VueJsTemplating\Templating;
 
 /**
  * @license GPL-2.0+
@@ -34,6 +36,11 @@
         * @var StatementSectionsView
         */
        private $statementSectionView;
+
+       /**
+        * @var string
+        */
+       private $languageCode;
 
        public function __construct(
                LocalizedTextProvider $textProvider,
@@ -66,6 +73,7 @@
                }
                $html .= '</div>';
                $html .= '</div>';
+               $html .= $this->getRepresentationsVueTemplate();
 
                return $html;
        }
@@ -76,9 +84,6 @@
         * @return string HTML
         */
        private function getFormHtml( Form $form ) {
-               //TODO Change to rendering all the representations
-               $representation = 
$form->getRepresentations()->getIterator()->current()->getText();
-
                $grammaticalFeaturesHtml = $this->templateFactory->render(
                        'wikibase-lexeme-form-grammatical-features',
                        [ implode(
@@ -93,13 +98,42 @@
                );
 
                return $this->templateFactory->render( 'wikibase-lexeme-form', [
-                       'some language',
-                       htmlspecialchars( $representation ),
-                       wfMessage( 'parentheses' )->rawParams( 
htmlspecialchars( $form->getId()->getSerialization() ) )
-                               ->text(),
+                       htmlspecialchars( $form->getId()->getSerialization() ),
+                       $this->renderRepresentationWidget( $form ),
                        $grammaticalFeaturesHtml,
                        $this->statementSectionView->getHtml( 
$form->getStatements() )
                ] );
+       }
+
+       /**
+        * @return string
+        */
+       private function renderRepresentationWidget( Form $form ) {
+               $templating = new Templating();
+
+               $representations = array_map(
+                       function ( Term $r ) {
+                               return [ 'value' => $r->getText(), 'language' 
=> $r->getLanguageCode() ];
+                       },
+                       iterator_to_array( $form->getRepresentations() )
+               );
+
+               $result = $templating->render(
+                       $this->getRawRepresentationVueTemplate(),
+                       [
+                               'inEditMode' => false,
+                               'representations' => $representations
+                       ],
+                       [
+                               'message' => function ( $key ) {
+                                       return $this->textProvider->get( $key );
+                               }
+                       ]
+               );
+
+               return '<div class="form-representations">'
+                       . $result
+                       . '</div>';
        }
 
        /**
@@ -110,4 +144,49 @@
                return $this->entityIdHtmlFormatter->formatEntityId( $id );
        }
 
+       private function getRepresentationsVueTemplate() {
+               return <<<HTML
+<script id="representation-widget-vue-template" type="x-template">
+       {$this->getRawRepresentationVueTemplate()}
+</script>
+HTML;
+       }
+
+       private function getRawRepresentationVueTemplate() {
+               return <<<'HTML'
+<div class="representation-widget">
+       <ul v-if="!inEditMode" 
class="representation-widget_representation-list">
+               <li v-for="representation in representations" 
class="representation-widget_representation">
+                       <span 
class="representation-widget_representation-value">{{representation.value}}</span>
+                       <span 
class="representation-widget_representation-language">
+                               {{representation.language}}
+                       </span>
+               </li>
+       </ul>
+       <div v-else>
+               <div class="representation-widget_edit-area">
+                       <ul class="representation-widget_representation-list">
+                               <li v-for="representation in representations" 
+                                       
class="representation-widget_representation-edit-box">
+                                       <input size="1" 
class="representation-widget_representation-value-input" 
+                                               v-model="representation.value">
+                                       <input size="1" 
class="representation-widget_representation-language-input" 
+                                               
v-model="representation.language">
+                                       <button 
class="representation-widget_representation-remove" 
+                                               
v-on:click="remove(representation)" 
+                                               
:title="'wikibase-remove'|message">
+                                               &times;
+                                       </button>
+                               </li>
+                               <li>
+                                       <button type="button" 
class="representation-widget_add" v-on:click="add" 
+                                               
:title="'wikibase-add'|message">+</button>
+                               </li>
+                       </ul>
+               </div>
+       </div>
+</div>
+HTML;
+       }
+
 }
diff --git a/src/WikibaseLexeme.hooks.php b/src/WikibaseLexeme.hooks.php
index b950c1e..06db641 100644
--- a/src/WikibaseLexeme.hooks.php
+++ b/src/WikibaseLexeme.hooks.php
@@ -112,6 +112,7 @@
                                'tests/qunit/widgets/LemmaWidgetStore.tests.js',
                                'tests/qunit/widgets/LemmaWidget.tests.js',
                                'tests/qunit/widgets/GlossWidget.tests.js',
+                               
'tests/qunit/widgets/RepresentationWidget.tests.js',
                        ],
                        'dependencies' => [
                                'jquery.valueview.tests.testExpert',
@@ -133,6 +134,7 @@
                                
'wikibase.lexeme.widgets.LemmaWidget.newLemmaWidgetStore',
                                
'wikibase.lexeme.widgets.LemmaWidget.newLemmaWidget',
                                'wikibase.lexeme.widgets.GlossWidget',
+                               'wikibase.lexeme.widgets.RepresentationWidget',
                                'wikibase.tests.qunit.testrunner',
                                'vue',
                                'vuex',
diff --git a/tests/browser/features/forms.feature 
b/tests/browser/features/forms.feature
index d15175a..e6f1de0 100644
--- a/tests/browser/features/forms.feature
+++ b/tests/browser/features/forms.feature
@@ -11,7 +11,7 @@
     Then Forms header should be there
      And Forms container should be there
      And for each Form there is a representation and an ID
-     And each representation is enclosed in tag having lang attribute with 
"some language" as a value
+     And each representation has a language
 
   @integration
   Scenario: View Forms grammatical features
@@ -37,20 +37,23 @@
     Then the first Form should no longer have the removed grammatical feature
 
   @integration
-  Scenario: Change representation
-   Given I have a Lexeme with a Form
+  Scenario: Change multi-variant representations
+    Given I have a Lexeme with a Form
      And I am on the page of the Lexeme to test
     When I click on the first Form's edit button
-     And I enter "new-representation" as the form representation
+     And I enter "colors" as the "en-us" form representation
+     And I click on the add representation button
+     And I enter "colours" as the "en-gb" form representation
      And I save the Form
-    Then "new-representation" should be displayed as a representation of the 
Form
+    Then "colors" should be displayed as the "en-us" representation of the Form
+     And "colours" should be displayed as the "en-gb" representation of the 
Form
 
   @integration
   Scenario: Add Form
     When I click the Forms list add button
-     And I enter "whatever" as the form representation
+     And I enter "whatever" as the "en" form representation
      And I save the Form
-    Then "whatever" should be displayed as a representation of the Form
+    Then "whatever" should be displayed as the "en" representation of the Form
 
 
   @integration
@@ -64,7 +67,7 @@
     Given I have the following properties with datatype:
       | stringprop | string |
       And I click the Forms list add button
-      And I enter "newForm" as the form representation
+      And I enter "newForm" as the "en" form representation
       And I save the Form
      When I click add statement on the Form
       And I select the claim property stringprop
diff --git a/tests/browser/features/step_definitions/forms_steps.rb 
b/tests/browser/features/step_definitions/forms_steps.rb
index 446bb17..d561f20 100644
--- a/tests/browser/features/step_definitions/forms_steps.rb
+++ b/tests/browser/features/step_definitions/forms_steps.rb
@@ -10,11 +10,11 @@
   expect(on(LexemePage).forms.count).to be > 0
 end
 
-
 Then(/^for each Form there is a representation and an ID$/) do
-  #todo: this only checks if there is at least one id and representation
-  expect(on(LexemePage).form_representation?).to be true
-  expect(on(LexemePage).form_id?).to be true
+  on(LexemePage).forms.each do |form|
+    expect(form.representations.count).to be > 0
+    expect(form.id?).to be true
+  end
 end
 
 Then(/^for each Form there is a statement list$/) do
@@ -23,9 +23,12 @@
   end
 end
 
-Then(/^each representation is enclosed in tag having lang attribute with 
"(.+)" as a value$/) do |value|
-  #todo: this only checks if there is at least one lang attribute
-  on(LexemePage).form_representation_element.attribute('lang').should == value
+Then(/^each representation has a language$/) do
+  on(LexemePage).forms.each do |form|
+    form.representations.each do |representation|
+      expect(representation.language?).to be true
+    end
+  end
 end
 
 Given(/^for each Form there is a grammatical feature list$/) do
@@ -39,9 +42,11 @@
   @form_I_am_currently_editing = on(LexemePage).forms[-1]
 end
 
-When(/^I enter "(.+)" as the form representation$/) do |representation|
-  @form_I_am_currently_editing.representation_input_element.when_visible.clear
-  @form_I_am_currently_editing.representation_input = representation
+When(/^I enter "(.*?)" as the "(.*?)" form representation$/) do 
|representation, language|
+  last_representation = @form_I_am_currently_editing.representations[-1]
+
+  last_representation.value_input = representation
+  last_representation.language_input = language
 end
 
 When(/^I save the Form$/) do
@@ -49,8 +54,17 @@
   @form_I_am_currently_editing.save_element.when_visible.click
 end
 
-Then(/^"(.+)" should be displayed as a representation of the Form$/) do 
|representation|
-  @form_I_am_currently_editing.representation_element.text.should == 
representation
+Then(/^"(.*?)" should be displayed as the "(.*?)" representation of the 
Form$/) do |value, language|
+  has_representation_with_value = 
@form_I_am_currently_editing.representations.any? do |representation|
+    representation.value_element.when_visible.text == value
+    representation.language_element.when_visible.text == language
+  end
+
+  expect(has_representation_with_value).to be true
+end
+
+When(/^I click on the add representation button$/) do
+  @form_I_am_currently_editing.add_representation_element.when_visible.click
 end
 
 Given(/^I have a Lexeme with a Form$/) do
diff --git a/tests/browser/features/support/pages/lexeme_page.rb 
b/tests/browser/features/support/pages/lexeme_page.rb
index 403f86b..335ad09 100644
--- a/tests/browser/features/support/pages/lexeme_page.rb
+++ b/tests/browser/features/support/pages/lexeme_page.rb
@@ -27,24 +27,38 @@
   a(:delete_button, css: '.oo-ui-buttonElement > .oo-ui-buttonElement-button')
 end
 
+class FormRepresentation
+  include PageObject
+
+  text_field(:value_input, class: 
'representation-widget_representation-value-input')
+  text_field(:language_input, class: 
'representation-widget_representation-language-input')
+  span(:value, class: 'representation-widget_representation-value')
+  span(:language, class: 'representation-widget_representation-language')
+end
+
 class LexemeForm
   include PageObject
 
-  span(:representation, class: 'wikibase-lexeme-form-text')
+  div(:id, class: 'wikibase-lexeme-form-id')
   div(:grammatical_feature_list, class: 
'wikibase-lexeme-form-grammatical-features')
   div(:statements, class: 'wikibase-statementgrouplistview')
-  textarea(:representation_input, css: '.wikibase-lexeme-form-text > textarea')
   text_field(:grammatical_features_input, css: 
'.wikibase-lexeme-form-grammatical-features-values input')
   a(:save, css: '.wikibase-toolbar-button-save > a')
   a(:cancel, css: '.wikibase-toolbar-button-cancel > a')
   a(:edit, css: '.wikibase-toolbar-button-edit > a')
   a(:grammatical_feature_selection_first_option, css: 
'.wikibase-lexeme-form-grammatical-features-values 
.oo-ui-menuOptionWidget:first-of-type a')
+  button(:add_representation, class: 'representation-widget_add')
 
   page_section(:statement_group, StatementGroup, class: 
'wikibase-statementgrouplistview')
   page_sections(
     :grammatical_features,
     GrammaticalFeatureValue,
     css: '.wikibase-lexeme-form-grammatical-features-values > span, 
.wikibase-lexeme-form-grammatical-features-values .oo-ui-tagItemWidget'
+  )
+  page_sections(
+    :representations,
+    FormRepresentation,
+    css: '.representation-widget_representation, 
.representation-widget_representation-edit-box'
   )
 
   def grammatical_feature?(label)
@@ -93,7 +107,6 @@
   span(:forms_header, id: 'forms')
   div(:forms_container, class: 'wikibase-lexeme-forms')
   h3(:form_representation, class: 'wikibase-lexeme-form-representation')
-  span(:form_id, class: 'wikibase-lexeme-form-id')
   span(:senses_header, id: 'senses')
   div(:senses_container, class: 'wikibase-lexeme-senses')
 
diff --git a/tests/phpunit/mediawiki/View/LexemeFormsViewTest.php 
b/tests/phpunit/mediawiki/View/LexemeFormsViewTest.php
index 2a31861..8c94a21 100644
--- a/tests/phpunit/mediawiki/View/LexemeFormsViewTest.php
+++ b/tests/phpunit/mediawiki/View/LexemeFormsViewTest.php
@@ -67,9 +67,32 @@
 
                assertThat(
                        $html,
-                       is( htmlPiece( havingChild(
-                               both( tagMatchingOutline( '<h3 lang="some 
language">' ) )
-                                       ->andAlso( havingTextContents( 
containsString( 'FORM_REPRESENTATION (F1)' ) ) )
+                       is( htmlPiece(
+                               both( havingChild(
+                                       allOf(
+                                               withClass( 
'representation-widget_representation-value' ),
+                                               havingTextContents( 
containsString( 'FORM_REPRESENTATION' ) )
+                                       ) ) )
+                               ->andAlso( havingChild(
+                                       allOf(
+                                               withClass( 
'representation-widget_representation-language' ),
+                                               havingTextContents( 
containsString( 'en' ) )
+                                       )
+                               ) ) ) )
+               );
+       }
+
+       public function testHtmlContainsFormId() {
+               $view = $this->newFormsView();
+               $html = $view->getHtml( [
+                       NewForm::havingId( 'F1' )->build()
+               ] );
+
+               assertThat(
+                       $html,
+                       is( htmlPiece(
+                               havingChild(
+                                       havingTextContents( containsString( 
'F1' ) )
                        ) ) )
                );
        }
@@ -106,7 +129,15 @@
                return new LexemeFormsView(
                        new DummyLocalizedTextProvider(),
                        new LexemeTemplateFactory( [
-                               'wikibase-lexeme-form' => '<h3 lang="$1">$2 
$3</h3>$4 $5',
+                               'wikibase-lexeme-form' => '
+                                       <div class="wikibase-lexeme-form">
+                                               <div 
class="wikibase-lexeme-form-header">
+                                                       <div 
class="wikibase-lexeme-form-id">$1</div>
+                                                       <div 
class="form-representations">$2</div>
+                                               </div>
+                                               $3
+                                               $4
+                                       </div>',
                                'wikibase-lexeme-form-grammatical-features' => 
'<div>$1</div>'
                        ] ),
                        new EntityIdHtmlLinkFormatter(
diff --git a/tests/qunit/jquery.wikibase.lexemeformview.tests.js 
b/tests/qunit/jquery.wikibase.lexemeformview.tests.js
index e029746..89656f1 100644
--- a/tests/qunit/jquery.wikibase.lexemeformview.tests.js
+++ b/tests/qunit/jquery.wikibase.lexemeformview.tests.js
@@ -4,24 +4,29 @@
 ( function ( $, wb, QUnit ) {
        'use strict';
 
-       var TEST_LEXMEFORMVIEW_CLASS = 'test_lexemeformview';
-
        /** @type {wikibase.datamodel.TermMap}*/
        var TermMap = wb.datamodel.TermMap;
        /** @type {wikibase.datamodel.Term}*/
        var Term = wb.datamodel.Term;
 
+       var selector = {
+               representationTextInput: 
'.representation-widget_representation-value-input',
+               representationLanguageInput: 
'.representation-widget_representation-language-input',
+               representationText: 
'.representation-widget_representation-value'
+       };
+
        QUnit.module( 'jquery.wikibase.lexemeformview', QUnit.newMwEnvironment( 
{
-               teardown: function () {
-                       $( '.' + TEST_LEXMEFORMVIEW_CLASS ).remove();
+               setup: function () {
+                       $( '<script id="representation-widget-vue-template" 
type="x-template"/>' )
+                               .html( getRepresentationWidgetTemplate() )
+                               .appendTo( '#qunit-fixture' );
                }
        } ) );
 
        var newLexemeFormView = function ( options ) {
-               var $node = $( '<div/>' ).appendTo( 'body' );
-               options = options || {};
+               var $node = $( '<div/>' ).appendTo( '#qunit-fixture' );
 
-               $node.addClass( TEST_LEXMEFORMVIEW_CLASS );
+               options = options || {};
 
                options.api = options.api || {};
                options.language = options.language || 'en';
@@ -32,7 +37,7 @@
                };
                options.buildStatementGroupListView = function () {};
 
-               return $node.lexemeformview( options || {} ).data( 
'lexemeformview' );
+               return $node.lexemeformview( options ).data( 'lexemeformview' );
        };
 
        var newForm = function ( id, defaultRepresentation ) {
@@ -66,13 +71,21 @@
        } );
 
        QUnit.test( 'value() creates value from input if it is in edit mode', 
function ( assert ) {
+               var done = assert.async();
                var view = newLexemeFormView(),
                        textInput = 'foobar';
 
-               view.startEditing();
-               view.element.find( view.options.inputNodeName ).val( textInput 
);
+               view.startEditing().then( function () {
+                       changeInputValue( view.element.find( 
selector.representationLanguageInput ), 'en' );
+                       changeInputValue( view.element.find( 
selector.representationTextInput ), textInput );
 
-               assert.equal( view.value().getRepresentations().getItemByKey( 
'en' ).getText(), textInput );
+                       assert.equal(
+                               view.value().getRepresentations().getItemByKey( 
'en' ).getText(),
+                               textInput
+                       );
+               } ).catch( function ( e ) {
+                       assert.notOk( e.stack );
+               } ).then( done );
        } );
 
        QUnit.test( 'should not be in edit mode when initialized without a 
value', function ( assert ) {
@@ -84,30 +97,88 @@
        } );
 
        QUnit.test( 'draws value in input node after startEditing()', function 
( assert ) {
+               var done = assert.async();
                var form = newForm( 'F123', 'foobar' ),
                        view = newLexemeFormView( {
                                value: form
                        } );
 
-               view.startEditing();
-               assert.equal(
-                       view.element.find( view.options.inputNodeName ).val(),
-                       form.getRepresentations().getItemByKey( 'en' ).getText()
-               );
+               view.startEditing().then( function () {
+                       assert.equal(
+                               view.element.find( 
selector.representationTextInput ).val(),
+                               form.getRepresentations().getItemByKey( 'en' 
).getText()
+                       );
+               } ).catch( function ( e ) {
+                       assert.notOk( e.stack );
+               } ).then( done );
+
        } );
 
        QUnit.test( 'draws value in text node after stopEditing()', function ( 
assert ) {
+               var done = assert.async();
+
                var form = newForm( 'F123', 'foobar' ),
                        view = newLexemeFormView( {
                                value: form
                        } );
 
-               view.startEditing();
-               view.stopEditing();
-               assert.equal(
-                       view.element.find( '.wikibase-lexeme-form-text' 
).text(),
-                       form.getRepresentations().getItemByKey( 'en' ).getText()
-               );
+               view.startEditing().then( function () {
+                       return view.stopEditing();
+               } ).then( function () {
+                       assert.equal(
+                               view.element.find( selector.representationText 
).text(),
+                               form.getRepresentations().getItemByKey( 'en' 
).getText()
+                       );
+               } ).catch( function ( e ) {
+                       assert.notOk( e.stack );
+               } ).then( done );
        } );
 
+       /**
+        * Sets input value and triggers 'input'
+        * @param {jQuery}$element
+        * @param {string} newValue
+        */
+       function changeInputValue( $element, newValue ) {
+               $element.val( newValue );
+               var event = document.createEvent( 'Event' );
+               event.initEvent( 'input', true, true );
+               $element[ 0 ].dispatchEvent( event );
+       }
+
+       function getRepresentationWidgetTemplate() {
+               return '<div class="representation-widget">\n' +
+                       '<ul v-if="!inEditMode" 
class="representation-widget_representation-list">\n' +
+                       '<li v-for="representation in representations" 
class="representation-widget_representation">\n' +
+                       '<span 
class="representation-widget_representation-value">{{representation.value}}</span>\n'
 +
+                       '<span 
class="representation-widget_representation-language">\n' +
+                       '{{representation.language}}\n' +
+                       '</span>\n' +
+                       '</li>\n' +
+                       '</ul>\n' +
+                       '<div v-else>\n' +
+                       '<div class="representation-widget_edit-area">\n' +
+                       '<ul 
class="representation-widget_representation-list">\n' +
+                       '<li v-for="representation in representations" \n' +
+                       
'class="representation-widget_representation-edit-box">\n' +
+                       '<input size="1" 
class="representation-widget_representation-value-input" \n' +
+                       'v-model="representation.value">\n' +
+                       '<input size="1" 
class="representation-widget_representation-language-input" \n' +
+                       'v-model="representation.language">\n' +
+                       '<button 
class="representation-widget_representation-remove" \n' +
+                       'v-on:click="remove(representation)" \n' +
+                       ':title="\'wikibase-remove\'|message">\n' +
+                       '&times;\n' +
+                       '</button>\n' +
+                       '</li>\n' +
+                       '<li>\n' +
+                       '<button type="button" 
class="representation-widget_add" v-on:click="add" \n' +
+                       ':title="\'wikibase-add\'|message">+</button>\n' +
+                       '</li>\n' +
+                       '</ul>\n' +
+                       '</div>\n' +
+                       '</div>\n' +
+                       '</div>';
+       }
+
 }( jQuery, wikibase, QUnit ) );
diff --git a/tests/qunit/widgets/RepresentationWidget.tests.js 
b/tests/qunit/widgets/RepresentationWidget.tests.js
new file mode 100644
index 0000000..854d233
--- /dev/null
+++ b/tests/qunit/widgets/RepresentationWidget.tests.js
@@ -0,0 +1,116 @@
+/**
+ * @license GPL-2.0+
+ */
+( function ( wb, $, QUnit ) {
+       'use strict';
+
+       var RepresentationWidget = require( 
'wikibase.lexeme.widgets.RepresentationWidget' );
+
+       var EMPTY_REPRESENTATION = { language: '', value: '' };
+       var SOME_REPRESENTATION = { language: 'en', value: 'representation in 
english' };
+
+       QUnit.module(
+               'wikibase.lexeme.widgets.RepresentationWidget',
+               function () {
+                       QUnit.test(
+                               'widget without representations, start editing 
- new empty representation is added',
+                               function ( assert ) {
+                                       var widget = newWidget( [] );
+
+                                       widget.edit();
+
+                                       assert.deepEqual( 
widget.representations, [ EMPTY_REPRESENTATION ] );
+                               }
+                       );
+
+                       QUnit.test(
+                               'widget with representation, start editing - 
only initial representation present',
+                               function ( assert ) {
+                                       var widget = newWidget( [ 
SOME_REPRESENTATION ] );
+
+                                       widget.edit();
+
+                                       assert.deepEqual(
+                                               widget.representations,
+                                               [ SOME_REPRESENTATION ]
+                                       );
+                               }
+                       );
+
+                       QUnit.test( 'when created - not in edit mode', function 
( assert ) {
+                               var widget = newWidget( [] );
+
+                               assert.notOk( widget.inEditMode );
+                       } );
+
+                       QUnit.test( 'switch to edit mode', function ( assert ) {
+                               var widget = newWidget( [] );
+
+                               widget.edit();
+
+                               assert.ok( widget.inEditMode );
+                       } );
+
+                       QUnit.test( 'not in edit mode after stopEditing was 
called', function ( assert ) {
+                               var widget = newWidget( [] );
+
+                               widget.edit();
+                               widget.stopEditing();
+
+                               assert.notOk( widget.inEditMode );
+                       } );
+
+                       QUnit.test( 'add a representation - empty 
representation added', function ( assert ) {
+                               var widget = newWidget(
+                                       [ SOME_REPRESENTATION ]
+                               );
+
+                               widget.edit();
+                               widget.add();
+
+                               assert.deepEqual( widget.representations[ 1 ], 
EMPTY_REPRESENTATION );
+                       } );
+
+                       QUnit.test( 'remove a representation', function ( 
assert ) {
+                               var widget = newWidget(
+                                       [ SOME_REPRESENTATION ]
+                               );
+
+                               widget.edit();
+                               widget.remove( SOME_REPRESENTATION );
+
+                               assert.deepEqual( widget.representations, [] );
+                       } );
+
+                       QUnit.test( 'cannot add representation if not in edit 
mode', function ( assert ) {
+                               var widget = newWidget( [] );
+
+                               assert.throws( function () {
+                                       widget.add();
+                               }, Error );
+                       } );
+
+                       QUnit.test( 'cannot remove representation if not in 
edit mode', function ( assert ) {
+                               var widget = newWidget(
+                                       [ SOME_REPRESENTATION ]
+                               );
+
+                               assert.throws( function () {
+                                       widget.remove( SOME_REPRESENTATION );
+                               }, Error );
+                       } );
+
+               }
+       );
+
+       function newWidget( representations ) {
+               return RepresentationWidget.create(
+                       representations,
+                       document.createElement( 'div' ),
+                       '<div id="dummy-template"></div>',
+                       function () {
+                       }
+               );
+       }
+
+}( wikibase, jQuery, QUnit ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/362180
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I5dda249fb2121db3b29804e202b9f90d3755a968
Gerrit-PatchSet: 19
Gerrit-Project: mediawiki/extensions/WikibaseLexeme
Gerrit-Branch: master
Gerrit-Owner: Jakob <[email protected]>
Gerrit-Reviewer: Aleksey Bekh-Ivanov (WMDE) <[email protected]>
Gerrit-Reviewer: Jonas Kress (WMDE) <[email protected]>
Gerrit-Reviewer: WMDE-leszek <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to