Add RL template module with HTML markup language
authorjdlrobson <jdlrobson@gmail.com>
Fri, 10 Oct 2014 00:07:14 +0000 (17:07 -0700)
committerjdlrobson <jdlrobson@gmail.com>
Fri, 17 Oct 2014 16:23:04 +0000 (09:23 -0700)
Preparation work for templating in core.
RL should allow us to ship HTML / template markup from server to client.
Use in Special:Upload and mediawiki.feedback as a proof of concept.
Separation of concerns etc...

See Also:
Ia63d6b6868f23a773e4a41daa0036d4bf2cd6724

Change-Id: I6ff38c12897e3164969a1090449e626001926c3b

22 files changed:
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderModule.php
maintenance/jsduck/categories.json
resources/Resources.php
resources/src/mediawiki.action/mediawiki.action.view.postEdit.js
resources/src/mediawiki.action/templates/postEdit.html [new file with mode: 0644]
resources/src/mediawiki.special/mediawiki.special.upload.js
resources/src/mediawiki.special/mediawiki.special.userlogin.common.js
resources/src/mediawiki.special/templates/captcha.html [new file with mode: 0644]
resources/src/mediawiki.special/templates/thumbnail.html [new file with mode: 0644]
resources/src/mediawiki/mediawiki.feedback.js
resources/src/mediawiki/mediawiki.js
resources/src/mediawiki/mediawiki.templates.js [new file with mode: 0644]
resources/src/mediawiki/templates/dialog.html [new file with mode: 0644]
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php [new file with mode: 0644]
tests/phpunit/includes/resourceloader/templates/template.html [new file with mode: 0644]
tests/phpunit/includes/resourceloader/templates/template2.html [new file with mode: 0644]
tests/phpunit/includes/resourceloader/templates/template_awesome.handlebars [new file with mode: 0644]
tests/qunit/QUnitTestResources.php
tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js [new file with mode: 0644]

index 57deb00..a0b4852 100644 (file)
@@ -974,12 +974,20 @@ class ResourceLoader {
                                        case 'messages':
                                                $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
                                                break;
+                                       case 'templates':
+                                               $out .= Xml::encodeJsCall(
+                                                       'mw.templates.set',
+                                                       array( (object)$module->getTemplates() ),
+                                                       ResourceLoader::inDebugMode()
+                                               );
+                                               break;
                                        default:
                                                $out .= self::makeLoaderImplementScript(
                                                        $name,
                                                        $scripts,
                                                        $styles,
-                                                       new XmlJsCode( $messagesBlob )
+                                                       new XmlJsCode( $messagesBlob ),
+                                                       $module->getTemplates()
                                                );
                                                break;
                                }
@@ -1044,10 +1052,14 @@ class ResourceLoader {
         * @param mixed $messages List of messages associated with this module. May either be an
         *   associative array mapping message key to value, or a JSON-encoded message blob containing
         *   the same data, wrapped in an XmlJsCode object.
+        * @param array $templates where keys are name of templates and values are the source of
+        *   the template.
         * @throws MWException
         * @return string
         */
-       public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
+       public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages,
+               $templates = array()
+       ) {
                if ( is_string( $scripts ) ) {
                        $scripts = new XmlJsCode( "function ( $, jQuery ) {\n{$scripts}\n}" );
                } elseif ( !is_array( $scripts ) ) {
@@ -1064,7 +1076,8 @@ class ResourceLoader {
                                // PHP/json_encode() consider empty arrays to be numerical arrays and
                                // output javascript "[]" instead of "{}". This fixes that.
                                (object)$styles,
-                               (object)$messages
+                               (object)$messages,
+                               (object)$templates,
                        ),
                        ResourceLoader::inDebugMode()
                );
index 7bbc9bb..6544813 100644 (file)
@@ -34,6 +34,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
        /** @var string Remote base path, see __construct() */
        protected $remoteBasePath = '';
 
+       /** @var array Saves a list of the templates named by the modules. */
+       protected $templates = array();
+
        /**
         * @var array List of paths to JavaScript files to always include
         * @par Usage:
@@ -268,6 +271,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 
                                        $this->{$member} = $option;
                                        break;
+                               // templates
+                               case 'templates':
+                                       $this->{$member} = (array) $option;
+                                       break;
                                // Single strings
                                case 'group':
                                case 'position':
@@ -544,6 +551,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                        $files[] = $this->skipFunction;
                }
                $files = array_map( array( $this, 'getLocalPath' ), $files );
+               // Templates
+               $templateFiles = array_map( array( $this, 'getLocalPath' ), $this->templates );
+               $files = array_merge( $files, $templateFiles );
                // File deps need to be treated separately because they're already prefixed
                $files = array_merge( $files, $this->getFileDependencies( $context->getSkin() ) );
 
@@ -959,4 +969,26 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
        protected function getLessCompiler( ResourceLoaderContext $context = null ) {
                return ResourceLoader::getLessCompiler( $this->getConfig() );
        }
+
+       /**
+        * Takes named templates by the module and adds them to the JavaScript output
+        *
+        * @return array of templates mapping template alias to content
+        */
+       function getTemplates() {
+               $templates = array();
+
+               foreach( $this->templates as $alias => $templatePath ) {
+                       // Alias is optional
+                       if ( is_int( $alias ) ) {
+                               $alias = $templatePath;
+                       }
+                       $localPath = $this->getLocalPath( $templatePath );
+                       if ( file_exists( $localPath ) ) {
+                               $content = file_get_contents( $localPath );
+                               $templates[ $alias ] = $content;
+                       }
+               }
+               return $templates;
+       }
 }
index 45eb70f..3f53ca9 100644 (file)
@@ -134,6 +134,16 @@ abstract class ResourceLoaderModule {
                return '';
        }
 
+       /**
+        * Returns JavaScript relating to adding templates to the client.
+        *
+        * @return string JavaScript code
+        */
+       public function getTemplates() {
+               // Stub, override expected.
+               return array();
+       }
+
        /**
         * @return Config
         * @since 1.24
index d6163bd..145749a 100644 (file)
@@ -13,7 +13,8 @@
                                        "mw.html",
                                        "mw.html.Cdata",
                                        "mw.html.Raw",
-                                       "mw.hook"
+                                       "mw.hook",
+                                       "mw.template"
                                ]
                        },
                        {
index acc937e..e893de6 100644 (file)
@@ -767,7 +767,10 @@ return array(
        /* MediaWiki */
 
        'mediawiki' => array(
-               'scripts' => 'resources/src/mediawiki/mediawiki.js',
+               'scripts' => array(
+                       'resources/src/mediawiki/mediawiki.js',
+                       'resources/src/mediawiki/mediawiki.templates.js',
+               ),
                'debugScripts' => 'resources/src/mediawiki/mediawiki.log.js',
                'raw' => true,
                'targets' => array( 'desktop', 'mobile' ),
@@ -851,6 +854,9 @@ return array(
                'position' => 'bottom',
        ),
        'mediawiki.feedback' => array(
+               'templates' => array(
+                       'dialog.html' => 'resources/src/mediawiki/templates/dialog.html',
+               ),
                'scripts' => 'resources/src/mediawiki/mediawiki.feedback.js',
                'styles' => 'resources/src/mediawiki/mediawiki.feedback.css',
                'dependencies' => array(
@@ -1063,6 +1069,9 @@ return array(
                ),
        ),
        'mediawiki.action.view.postEdit' => array(
+               'templates' => array(
+                       'postEdit.html' => 'resources/src/mediawiki.action/templates/postEdit.html',
+               ),
                'scripts' => 'resources/src/mediawiki.action/mediawiki.action.view.postEdit.js',
                'styles' => 'resources/src/mediawiki.action/mediawiki.action.view.postEdit.css',
                'dependencies' => array(
@@ -1337,6 +1346,9 @@ return array(
                'scripts' => 'resources/src/mediawiki.special/mediawiki.special.undelete.js',
        ),
        'mediawiki.special.upload' => array(
+               'templates' => array(
+                       'thumbnail.html' => 'resources/src/mediawiki.special/templates/thumbnail.html',
+               ),
                'scripts' => 'resources/src/mediawiki.special/mediawiki.special.upload.js',
                'messages' => array(
                        'widthheight',
@@ -1378,6 +1390,9 @@ return array(
                'position' => 'top',
        ),
        'mediawiki.special.userlogin.common.js' => array(
+               'templates' => array(
+                       'captcha.html' => 'resources/src/mediawiki.special/templates/captcha.html',
+               ),
                'scripts' => array(
                        'resources/src/mediawiki.special/mediawiki.special.userlogin.common.js',
                ),
index 4d2c47a..ee2246d 100644 (file)
                        data.message = $.parseHTML( mw.message( 'postedit-confirmation-saved', data.user || mw.user ).escaped() );
                }
 
-               $div = $(
-                       '<div class="postedit-container">' +
-                               '<div class="postedit">' +
-                                       '<div class="postedit-icon postedit-icon-checkmark postedit-content"></div>' +
-                                       '<a href="#" class="postedit-close">&times;</a>' +
-                               '</div>' +
-                       '</div>'
-               );
+               $div = $( mw.template.get( 'mediawiki.action.view.postEdit', 'postEdit.html' ).render() );
 
                if ( typeof data.message === 'string' ) {
                        $div.find( '.postedit-content' ).text( data.message );
diff --git a/resources/src/mediawiki.action/templates/postEdit.html b/resources/src/mediawiki.action/templates/postEdit.html
new file mode 100644 (file)
index 0000000..dbb482a
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="postedit-container">
+       <div class="postedit">
+               <div class="postedit-icon postedit-icon-checkmark postedit-content"></div>
+               <a href="#" class="postedit-close">&times;</a>
+       </div>
+</div>
index 04bc978..ab851b3 100644 (file)
                                ctx,
                                meta,
                                previewSize = 180,
-                               thumb = $( '<div id="mw-upload-thumbnail" class="thumb tright">' +
-                                                       '<div class="thumbinner">' +
-                                                               '<div class="mw-small-spinner" style="width: 180px; height: 180px"></div>' +
-                                                               '<div class="thumbcaption"><div class="filename"></div><div class="fileinfo"></div></div>' +
-                                                       '</div>' +
-                                               '</div>' );
+                               thumb = $( mw.template.get( 'mediawiki.special.upload', 'thumbnail.html' ).render() );
 
                        thumb.find( '.filename' ).text( file.name ).end()
                                .find( '.fileinfo' ).text( prettySize( file.size ) ).end();
index 247f814..ab02e4e 100644 (file)
@@ -9,6 +9,7 @@
        function adjustFancyCaptcha( $content, buttonSubmit ) {
                var $submit = $content.find( buttonSubmit ),
                        tabIndex,
+                       $el,
                        $captchaStuff,
                        $captchaImageContainer,
                        // JavaScript can't yet parse the message 'createacct-imgcaptcha-help' when it
 
                        // Insert another div before the submit button that will include the
                        // repositioned FancyCaptcha div, an input field, and possible help.
-                       $submit.closest( 'div' ).before( [
-                               '<div>',
-                                       '<label for="wpCaptchaWord">' + mw.message( 'createacct-captcha' ).escaped() + '</label>',
-                                       '<div class="mw-createacct-captcha-container">',
-                                               '<div class="mw-createacct-captcha-and-reload" />',
-                                               '<input id="wpCaptchaWord" class="mw-ui-input" name="wpCaptchaWord" type="text" placeholder="' +
-                                                       mw.message( 'createacct-imgcaptcha-ph' ).escaped() +
-                                                       '" tabindex="' + tabIndex + '" autocapitalize="off" autocorrect="off">',
-                                                       helpHtml,
-                                       '</div>',
-                               '</div>'
-                       ].join( '' ) );
+                       $el = $submit.closest( 'div' ).before(
+                               mw.template.get( 'mediawiki.special.userlogin.common.js', 'captcha.html' ).render() );
+                       $el.find( 'label' ).text( mw.msg( 'createacct-captcha' ) );
+                       $el.find( '#wpCaptchaWord' ).attr( 'tabindex', tabIndex ).
+                               attr( 'placeholder', mw.msg( 'createacct-imgcaptcha-ph' ) );
+                       $el.find( 'span' ).html( helpHtml );
 
                        // Stick the FancyCaptcha container inside our bordered and framed parents.
                        $captchaImageContainer
diff --git a/resources/src/mediawiki.special/templates/captcha.html b/resources/src/mediawiki.special/templates/captcha.html
new file mode 100644 (file)
index 0000000..d30f191
--- /dev/null
@@ -0,0 +1,9 @@
+<div>
+       <label for="wpCaptchaWord"></label>
+       <div class="mw-createacct-captcha-container">
+               <div class="mw-createacct-captcha-and-reload" />
+               <input id="wpCaptchaWord" class="mw-ui-input" name="wpCaptchaWord"
+                       type="text" autocapitalize="off" autocorrect="off">
+               <span/>
+       </div>
+</div>
diff --git a/resources/src/mediawiki.special/templates/thumbnail.html b/resources/src/mediawiki.special/templates/thumbnail.html
new file mode 100644 (file)
index 0000000..73042f2
--- /dev/null
@@ -0,0 +1,9 @@
+<div id="mw-upload-thumbnail" class="thumb tright">
+       <div class="thumbinner">
+               <div class="mw-small-spinner" style="width: 180px; height: 180px"></div>
+               <div class="thumbcaption">
+                       <div class="filename"></div>
+                       <div class="fileinfo"></div>
+               </div>
+       </div>
+</div>
index 1c0d833..01a75f2 100644 (file)
                                target: '_blank'
                        } );
 
-                       // TODO: Use a stylesheet instead of these inline styles
-                       this.$dialog =
-                               $( '<div style="position: relative;"></div>' ).append(
-                                       $( '<div class="feedback-mode feedback-form"></div>' ).append(
-                                               $( '<small>' ).append(
-                                                       $( '<p>' ).msg(
-                                                               'feedback-bugornote',
-                                                               $bugNoteLink,
-                                                               fb.title.getNameText(),
-                                                               $feedbackPageLink.clone()
-                                                       )
-                                               ),
-                                               $( '<div style="margin-top: 1em;"></div>' )
-                                               .msg( 'feedback-subject' )
-                                               .append(
-                                                       $( '<br>' ),
-                                                       $( '<input type="text" class="feedback-subject" name="subject" maxlength="60" style="width: 100%; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box;"/>' )
-                                               ),
-                                               $( '<div style="margin-top: 0.4em;"></div>' )
-                                               .msg( 'feedback-message' )
-                                               .append(
-                                                       $( '<br>' ),
-                                                       $( '<textarea name="message" class="feedback-message" rows="5" cols="60"></textarea>' )
-                                               )
-                                       ),
-                                       $( '<div class="feedback-mode feedback-bugs"></div>' ).append(
-                                               $( '<p>' ).msg( 'feedback-bugcheck', $bugsListLink )
-                                       ),
-                                       $( '<div class="feedback-mode feedback-submitting" style="text-align: center; margin: 3em 0;"></div>' )
-                                       .msg( 'feedback-adding' )
-                                       .append(
-                                               $( '<br>' ),
-                                               $( '<span class="feedback-spinner"></span>' )
-                                       ),
-                                       $( '<div class="feedback-mode feedback-thanks" style="text-align: center; margin:1em"></div>' ).msg(
-                                               'feedback-thanks', fb.title.getNameText(), $feedbackPageLink.clone()
-                                       ),
-                                       $( '<div class="feedback-mode feedback-error" style="position: relative;"></div>' ).append(
-                                               $( '<div class="feedback-error-msg style="color: #990000; margin-top: 0.4em;"></div>' )
-                                       )
-                               );
+                       // TODO: Use a stylesheet instead of these inline styles in the template
+                       this.$dialog = $( mw.template.get( 'mediawiki.feedback', 'dialog.html' ).render() );
+                       this.$dialog.find( '.feedback-mode small p' ).msg(
+                               'feedback-bugornote',
+                               $bugNoteLink,
+                               fb.title.getNameText(),
+                               $feedbackPageLink.clone()
+                       );
+                       this.$dialog.find( '.feedback-form .subject span' ).msg( 'feedback-subject' );
+                       this.$dialog.find( '.feedback-form .message span' ).msg( 'feedback-message' );
+                       this.$dialog.find( '.feedback-bugs p' ).msg( 'feedback-bugcheck', $bugsListLink );
+                       this.$dialog.find( '.feedback-submitting span' ).msg( 'feedback-adding' );
+                       this.$dialog.find( '.feedback-thanks' ).msg( 'feedback-thanks', fb.title.getNameText(),
+                               $feedbackPageLink.clone() );
 
                        this.$dialog.dialog( {
                                width: 500,
index e29c734..a070986 100644 (file)
                 */
                messages: new Map(),
 
+               /**
+                * Templates associated with a module
+                * @property {mw.Map}
+                */
+               templates: new Map(),
+
                /* Public Methods */
 
                /**
                         */
                        function execute( module ) {
                                var key, value, media, i, urls, cssHandle, checkCssHandles,
+                                       templates = {},
                                        cssHandlesRegistered = false;
 
                                if ( registry[module] === undefined ) {
                                        mw.messages.set( registry[module].messages );
                                }
 
+                               // Initialise templates
+                               if ( !$.isEmptyObject( registry[module].templates ) ) {
+                                       templates[module] = registry[module].templates;
+                                       mw.templates.set( templates );
+                               }
+
                                if ( $.isReady || registry[module].async ) {
                                        // Make sure we don't run the scripts until all (potentially asynchronous)
                                        // stylesheet insertions have completed.
                                 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
                                 *
                                 * @param {Object} msgs List of key/value pairs to be added to mw#messages.
+                                * @param {Object} templates List of key/value pairs to be added to mw#templates (optional)
                                 */
-                               implement: function ( module, script, style, msgs ) {
+                               implement: function ( module, script, style, msgs, templates ) {
                                        // Validate input
                                        if ( typeof module !== 'string' ) {
                                                throw new Error( 'module must be a string, not a ' + typeof module );
                                        if ( !$.isPlainObject( msgs ) ) {
                                                throw new Error( 'msgs must be an object, not a ' + typeof msgs );
                                        }
+                                       if ( templates && !$.isPlainObject( templates ) ) {
+                                               throw new Error( 'templates must be an object, not a ' + typeof templates );
+                                       }
                                        // Automatically register module
                                        if ( registry[module] === undefined ) {
                                                mw.loader.register( module );
                                        registry[module].script = script;
                                        registry[module].style = style;
                                        registry[module].messages = msgs;
+                                       registry[module].templates = templates;
                                        // The module may already have been marked as erroneous
                                        if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
                                                registry[module].state = 'loaded';
diff --git a/resources/src/mediawiki/mediawiki.templates.js b/resources/src/mediawiki/mediawiki.templates.js
new file mode 100644 (file)
index 0000000..a6d26e6
--- /dev/null
@@ -0,0 +1,120 @@
+/**
+ * @class mw.template
+ * @singleton
+ */
+( function ( mw ) {
+       var compiledTemplates = {},
+               compilers = {};
+
+       mw.template = {
+               /**
+                * Register a new compiler and template
+                * @method
+                * @param {String} name of compiler. Should also match with any file extensions of templates that want to use it.
+                * @param {Function} compiler which must implement a compile function
+                */
+               registerCompiler: function ( name, compiler ) {
+                       if ( compiler.compile ) {
+                               compilers[name] = compiler;
+                       } else {
+                               throw new Error( 'Template compiler must implement compile function.' );
+                       }
+               },
+               /**
+                * Work out which compiler is associated with the template based on its suffix
+                * @method
+                * @param {String} templateName Name of template to add including file extension
+                * @return {Function} compiler
+                */
+               getCompilerFromName: function ( templateName ) {
+                       var templateParts = templateName.split( '.' ), compiler,
+                               ext;
+
+                       if ( templateParts.length > 1 ) {
+                               ext = templateParts[ templateParts.length - 1 ];
+                               if ( compilers[ ext ] ) {
+                                       compiler = compilers[ ext ];
+                               } else {
+                                       throw new Error( 'Template compiler not found for: ' + ext );
+                               }
+                       } else {
+                               throw new Error( 'Template has no suffix. Unable to identify compiler.' );
+                       }
+                       return compiler;
+               },
+               /**
+                * Define a template. Compiles newly added templates based on
+                * the file extension of name and the available compilers.
+                * @method
+                * @param {String} moduleName Name of RL module to get template from
+                * @param {String} templateName Name of template to add including file extension
+                * @param {String} markup Associated markup (html)
+                * @return {Function} compiled template
+                */
+               add: function ( moduleName, templateName, markup ) {
+                       var compiledTemplate,
+                               compiler = this.getCompilerFromName( templateName );
+
+                       // check module has a compiled template cache
+                       compiledTemplates[moduleName] = compiledTemplates[moduleName] || {};
+
+                       compiledTemplate = compiler.compile( markup );
+                       compiledTemplates[moduleName][ templateName ] = compiledTemplate;
+                       return compiledTemplate;
+               },
+               /**
+                * Retrieve defined template
+                *
+                * @method
+                * @param {string} name Name of template to be retrieved
+                * @return {Object} template compiler
+                * accepts template data object as its argument.
+                */
+               get: function ( moduleName, templateName ) {
+                       var moduleTemplates;
+
+                       // check if the template has already been compiled, compile it if not
+                       if ( !compiledTemplates[ moduleName ] || !compiledTemplates[ moduleName ][ templateName ] ) {
+                               moduleTemplates = mw.templates.get( moduleName );
+                               if ( !moduleTemplates ) {
+                                       throw new Error( 'No templates associated with module: ' + moduleName );
+                               }
+
+                               if ( moduleTemplates[ templateName ] ) {
+                                       // add compiled version
+                                       this.add( moduleName, templateName, moduleTemplates[ templateName ] );
+                               } else {
+                                       throw new Error( 'Template in module ' + moduleName + ' not found: ' + templateName );
+                               }
+                       }
+                       return compiledTemplates[ moduleName ][ templateName ];
+               },
+               /**
+                * Wraps our template engine of choice
+                * @method
+                * @param {string} templateBody Template body.
+                * @param {string} compilerName The name of a registered compiler
+                * @return {Object} template interface
+                * accepts template data object as its argument.
+                */
+               compile: function ( templateBody, compilerName ) {
+                       var compiler = compilers[ compilerName ];
+                       if ( !compiler ) {
+                               throw new Error( 'Unknown compiler ' + compilerName );
+                       }
+                       return compiler.compile( templateBody );
+               }
+       };
+
+       // Register basic html compiler
+       mw.template.registerCompiler( 'html', {
+               compile: function ( src ) {
+                       return {
+                               render: function () {
+                                       return src;
+                               }
+                       };
+               }
+       } );
+
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki/templates/dialog.html b/resources/src/mediawiki/templates/dialog.html
new file mode 100644 (file)
index 0000000..e116f3e
--- /dev/null
@@ -0,0 +1,25 @@
+<div style="position: relative; display: block;" class="ui-dialog-content ui-widget-content">
+       <div class="feedback-mode feedback-form">
+               <small><p></p></small>
+               <div class="subject" style="margin-top: 1em;">
+                       <span></span><br>
+                       <input type="text" class="feedback-subject" name="subject" maxlength="60"
+                               style="width: 100%; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box;">
+               </div>
+               <div class="message" style="margin-top: 0.4em;">
+                       <span></span><br>
+                       <textarea name="message" class="feedback-message" rows="5" cols="60"></textarea>
+               </div>
+       </div>
+       <div class="feedback-mode feedback-bugs">
+               <p></p>
+       </div>
+       <div class="feedback-mode feedback-submitting" style="text-align: center; margin: 3em 0;">
+               <span></span><br>
+               <span class="feedback-spinner"></span>
+       </div>
+       <div class="feedback-mode feedback-thanks" style="text-align: center; margin:1em"></div>
+       <div class="feedback-mode feedback-error" style="position: relative;">
+               <div class="feedback-error-msg" style=" color:#990000; margin-top:0.4em;"></div>
+       </div>
+</div>
index d7e8cd3..89d1de7 100644 (file)
@@ -172,7 +172,7 @@ mw.test.baz({token:123});mw.loader.state({"test.quux":"ready"});
                        array(
                                array( 'test.quux', ResourceLoaderModule::TYPE_COMBINED ),
                                '<script>if(window.mw){
-mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"css":[".mw-icon{transition:none}\n"]},{});
+mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"css":[".mw-icon{transition:none}\n"]},{},{});
 
 }</script>
 '
diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php
new file mode 100644 (file)
index 0000000..474a01b
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * @group ResourceLoader
+ */
+class ResourceLoaderFileModuleTest extends MediaWikiTestCase {
+       static function getModules() {
+               $base = array(
+                       'localBasePath' => realpath( dirname( __FILE__ ) ),
+               );
+
+               return array(
+                       'noTemplateModule' => array(),
+
+                       'htmlTemplateModule' => $base + array(
+                               'templates' => array(
+                                       'templates/template.html',
+                                       'templates/template2.html',
+                               )
+                       ),
+
+                       'aliasedHtmlTemplateModule' => $base + array(
+                               'templates' => array(
+                                       'foo.html' => 'templates/template.html',
+                                       'bar.html' => 'templates/template2.html',
+                               )
+                       ),
+
+                       'templateModuleHandlebars' => $base + array(
+                               'templates' => array(
+                                       'templates/template_awesome.handlebars',
+                               ),
+                       ),
+               );
+       }
+
+       public function providerGetTemplates() {
+               $modules = self::getModules();
+
+               return array(
+                       array(
+                               $modules['noTemplateModule'],
+                               array(),
+                       ),
+                       array(
+                               $modules['templateModuleHandlebars'],
+                               array(
+                                       'templates/template_awesome.handlebars' => "wow\n",
+                               ),
+                       ),
+                       array(
+                               $modules['htmlTemplateModule'],
+                               array(
+                                       'templates/template.html' => "<strong>hello</strong>\n",
+                                       'templates/template2.html' => "<div>goodbye</div>\n",
+                               ),
+                       ),
+                       array(
+                               $modules['aliasedHtmlTemplateModule'],
+                               array(
+                                       'foo.html' => "<strong>hello</strong>\n",
+                                       'bar.html' => "<div>goodbye</div>\n",
+                               ),
+                       ),
+               );
+       }
+
+       public function providerGetModifiedTime() {
+               $modules = self::getModules();
+
+               return array(
+                       // Check the default value when no templates present in module is 1
+                       array( $modules['noTemplateModule'], 1 ),
+               );
+       }
+
+       // tests
+       /**
+        * @dataProvider providerGetTemplates
+        */
+       public function testGetTemplates( $module, $expected ) {
+               $rl = new ResourceLoaderFileModule( $module );
+
+               $this->assertEquals( $rl->getTemplates(), $expected );
+       }
+
+       /**
+        * @dataProvider providerGetModifiedTime
+        */
+       public function testGetModifiedTime( $module, $expected ) {
+               $rl = new ResourceLoaderFileModule( $module );
+               $ts = $rl->getModifiedTime( new ResourceLoaderContext(
+                       new ResourceLoader, new WebRequest() ) );
+               $this->assertEquals( $ts, $expected );
+       }
+}
diff --git a/tests/phpunit/includes/resourceloader/templates/template.html b/tests/phpunit/includes/resourceloader/templates/template.html
new file mode 100644 (file)
index 0000000..1f6a7d2
--- /dev/null
@@ -0,0 +1 @@
+<strong>hello</strong>
diff --git a/tests/phpunit/includes/resourceloader/templates/template2.html b/tests/phpunit/includes/resourceloader/templates/template2.html
new file mode 100644 (file)
index 0000000..a322f67
--- /dev/null
@@ -0,0 +1 @@
+<div>goodbye</div>
diff --git a/tests/phpunit/includes/resourceloader/templates/template_awesome.handlebars b/tests/phpunit/includes/resourceloader/templates/template_awesome.handlebars
new file mode 100644 (file)
index 0000000..5f5c07d
--- /dev/null
@@ -0,0 +1 @@
+wow
index 34007ed..44d6efc 100644 (file)
@@ -66,6 +66,7 @@ return array(
                        'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js',
+                       'tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js',
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js
new file mode 100644 (file)
index 0000000..96413f1
--- /dev/null
@@ -0,0 +1,51 @@
+( function ( mw, $ ) {
+
+       QUnit.module( 'Templates', {
+               setup: function () {
+                       var abcCompiler = {
+                               registerPartial: $.noop,
+                               compile: function () {
+                                       return 'abc default compiler';
+                               }
+                       };
+                       // Register some template compiler languages
+                       mw.template.registerCompiler( 'abc', abcCompiler );
+                       mw.template.registerCompiler( 'xyz', {
+                               registerPartial: $.noop,
+                               compile: function () {
+                                       return 'xyz compiler';
+                               }
+                       } );
+
+                       // register some templates
+                       mw.templates.set( {
+                               'test.mediawiki.templates': {
+                                       'test_templates_foo.xyz': 'goodbye',
+                                       'test_templates_foo.abc': 'thankyou'
+                               }
+                       } );
+               }
+       } );
+
+       QUnit.test( 'Template, getCompiler - default case', 4, function ( assert ) {
+               assert.throws( function () {
+                               mw.template.add( 'module', 'test_templates_foo', 'hello' );
+                       }, 'When no prefix throw exception.' );
+               assert.throws( function () {
+                               mw.template.compile( '{{foo}}', 'rainbow' );
+                       }, 'Unknown compiler names throw exceptions.' );
+               assert.strictEqual( mw.template.get( 'test.mediawiki.templates', 'test_templates_foo.xyz' ), 'xyz compiler' );
+               assert.strictEqual( mw.template.get( 'test.mediawiki.templates', 'test_templates_foo.abc' ), 'abc default compiler' );
+       } );
+
+       QUnit.test( 'Template, get module that is not loaded.', 2, function ( assert ) {
+               assert.throws( function () {
+                               mw.template.get( 'this.should.not.exist', 'hello' );
+                       }, 'When bad module name given throw error.' );
+
+               assert.throws( function () {
+                               mw.template.get( 'mediawiki.templates', 'hello' );
+                       }, 'The template hello should not exist in the mediawiki.templates module and should throw an exception.' );
+       } );
+
+}( mediaWiki, jQuery ) );