WMDE-leszek has submitted this change and it was merged. Change subject: Initial commit. ......................................................................
Initial commit. This turns the prototype code of the Revision Slider into a MediaWiki extension. With this extension enabled the slider should appear on diff pages already but it won't actually show diffs when adjusting the slider pointers. Things that changed so far in the prototype JS code: - pulled out the rainbow function and made it use Math.floor instead of the ~~ binary operator - pulled out the API request that fetches the revisions into its own module - cleaned up and documented some parts of the code The code is still very much WIP and JSCS still has a couple of complaints. Bug: T132576 Change-Id: I2e22365f3b93a76d5b8d3997242b5fed996c6d78 --- A .gitignore A .jscsrc A .jshintignore A .jshintrc A Gruntfile.js A RevisionSlider.hooks.php A RevisionSlider.php A composer.json A extension.json A i18n/en.json A i18n/qqq.json A modules/ext.RevisionSlider.css A modules/ext.RevisionSlider.fetchRevisions.js A modules/ext.RevisionSlider.init.js A modules/ext.RevisionSlider.rainbow.js A package.json A phpcs.xml A tests/RevisionSlider.test.js A tests/RevisionSlider.test.php 19 files changed, 702 insertions(+), 0 deletions(-) Approvals: WMDE-Fisch: Verified; Looks good to me, but someone else must approve WMDE-leszek: Verified; Looks good to me, approved jenkins-bot: Verified diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbc2c3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +composer.lock +node_modules/ +vendor/ diff --git a/.jscsrc b/.jscsrc new file mode 100755 index 0000000..a031ea4 --- /dev/null +++ b/.jscsrc @@ -0,0 +1,3 @@ +{ + "preset": "wikimedia" +} \ No newline at end of file diff --git a/.jshintignore b/.jshintignore new file mode 100755 index 0000000..b512c09 --- /dev/null +++ b/.jshintignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100755 index 0000000..69882aa --- /dev/null +++ b/.jshintrc @@ -0,0 +1,22 @@ +{ + // Enforcing + "bitwise": true, + "eqeqeq": true, + "es3": true, + "latedef": true, + "noarg": true, + "nonew": true, + "undef": true, + "unused": true, + "strict": false, + + // Environment + "browser": true, + + "globals": { + "mw": false, + "$": false, + "mediaWiki": false, + "jQuery": false + } +} diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100755 index 0000000..56ff218 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,35 @@ +/*jshint node:true */ +module.exports = function ( grunt ) { + grunt.loadNpmTasks( 'grunt-contrib-jshint' ); + grunt.loadNpmTasks( 'grunt-jsonlint' ); + grunt.loadNpmTasks( 'grunt-banana-checker' ); + grunt.loadNpmTasks( 'grunt-jscs' ); + + grunt.initConfig( { + jshint: { + options: { + jshintrc: true + }, + all: [ + '*.js', + 'modules/**/*.js' + ] + }, + jscs: { + src: '<%= jshint.all %>' + }, + banana: { + all: 'i18n/' + }, + jsonlint: { + all: [ + '*.json', + '**/*.json', + '!node_modules/**' + ] + } + } ); + + grunt.registerTask( 'test', [ 'jshint', 'jscs', 'jsonlint', 'banana' ] ); + grunt.registerTask( 'default', 'test' ); +}; diff --git a/RevisionSlider.hooks.php b/RevisionSlider.hooks.php new file mode 100755 index 0000000..0efe5a3 --- /dev/null +++ b/RevisionSlider.hooks.php @@ -0,0 +1,20 @@ +<?php + +/** + * RevisionSlider extension hooks + * + * @file + * @ingroup Extensions + * @license GPL-2.0+ + */ +class RevisionSliderHooks { + public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) { + if ( self::isRevisionPage( $skin->getContext()->getRequest() ) ) { + $out->addModules( 'ext.RevisionSlider.init' ); + } + } + + private static function isRevisionPage( WebRequest $request ) { + return $request->getVal( 'action' ) === 'history' || $request->getVal( 'type' ) === 'revision'; + } +} diff --git a/RevisionSlider.php b/RevisionSlider.php new file mode 100644 index 0000000..6df2e0a --- /dev/null +++ b/RevisionSlider.php @@ -0,0 +1,36 @@ +<?php +/** + * Revision Slider MediaWiki Extension + */ + +$wgAutoloadClasses['RevisionSliderHooks'] = __DIR__ . '/RevisionSlider.hooks.php'; +$wgHooks['BeforePageDisplay'][] = 'RevisionSliderHooks::onBeforePageDisplay'; + +$wgResourceModules['ext.RevisionSlider.init'] = [ + 'scripts' => [ + 'modules/ext.RevisionSlider.init.js', + ], + 'styles' => [ + 'modules/ext.RevisionSlider.css', + ], + 'dependencies' => [ + 'ext.RevisionSlider.rainbow', + 'ext.RevisionSlider.fetchRevisions', + ], + + 'localBasePath' => __DIR__, +]; + +$wgResourceModules['ext.RevisionSlider.rainbow'] = [ + 'scripts' => [ + 'modules/ext.RevisionSlider.rainbow.js', + ], + 'localBasePath' => __DIR__, +]; + +$wgResourceModules['ext.RevisionSlider.fetchRevisions'] = [ + 'scripts' => [ + 'modules/ext.RevisionSlider.fetchRevisions.js', + ], + 'localBasePath' => __DIR__, +]; diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..a35cc39 --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "require-dev": { + "jakub-onderka/php-parallel-lint": "0.9", + "mediawiki/mediawiki-codesniffer": "0.4.0" + }, + "scripts": { + "test": [ + "parallel-lint . --exclude vendor", + "phpcs -sp" + ], + "fix": "phpcbf" + } +} diff --git a/extension.json b/extension.json new file mode 100755 index 0000000..8fb3887 --- /dev/null +++ b/extension.json @@ -0,0 +1,54 @@ +{ + "name": "RevisionSlider", + "version": "0.0.0", + "author": [ + "WMDE" + ], + "url": "https://phabricator.wikimedia.org/project/profile/1639/", + "namemsg": "revisionSlider", + "descriptionmsg": "revisionSlider-desc", + "license-name": "GPL-2.0+", + "type": "other", + "manifest_version": 1, + "MessageDirs": { + "RevisionSlider": [ + "i18n" + ] + }, + "AutoloadClasses": { + "RevisionSliderHooks": "RevisionSlider.hooks.php" + }, + "ResourceModules": { + "ext.RevisionSlider.init": { + "scripts": [ + "modules/ext.RevisionSlider.init.js" + ], + "styles": [ + "modules/ext.RevisionSlider.css" + ], + "dependencies": [ + "ext.RevisionSlider.rainbow", + "ext.RevisionSlider.fetchRevisions" + ], + "position": "top" + }, + "ext.RevisionSlider.rainbow": { + "scripts": [ + "modules/ext.RevisionSlider.rainbow.js" + ] + }, + "ext.RevisionSlider.fetchRevisions": { + "scripts": [ + "modules/ext.RevisionSlider.fetchRevisions.js" + ] + } + }, + "ResourceFileModulePaths": { + "localBasePath": "" + }, + "Hooks": { + "BeforePageDisplay": [ + "RevisionSliderHooks::onBeforePageDisplay" + ] + } +} diff --git a/i18n/en.json b/i18n/en.json new file mode 100755 index 0000000..173a79f --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "WMDE" + ] + }, + "revisionSlider": "RevisionSlider", + "revisionSlider-desc": "" +} \ No newline at end of file diff --git a/i18n/qqq.json b/i18n/qqq.json new file mode 100755 index 0000000..7e056b5 --- /dev/null +++ b/i18n/qqq.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "WMDE" + ] + }, + "revisionSlider": "Revision Slider", + "revisionSlider-desc": "{{desc|name=RevisionSlider|url=}}" +} diff --git a/modules/ext.RevisionSlider.css b/modules/ext.RevisionSlider.css new file mode 100755 index 0000000..3aeaa12 --- /dev/null +++ b/modules/ext.RevisionSlider.css @@ -0,0 +1,98 @@ +.ui-slider-tick-mark { + display: inline-block; + border: 1px solid black; + height: 9px; + position: absolute; + top: -3px; + opacity: 0.35; + z-index: 10; +} + +.ui-slider-tick-mark:hover { + opacity: 0.65!important; + -webkit-transition: all ease 0.2s; + -moz-transition: all ease 0.2s; + -o-transition: all ease 0.2s; + transition: all ease 0.2s; +} + +.rvslider-legend-box { + opacity: 0.35; + font-size: 1.4em; +} + +.revisions { + height: 150px; +} +.revisions-container { + width: 90%; + position: relative; + overflow: hidden; +} +.revisions-container, .arrow { + float: left; +} +.arrow { + width: 5%; + margin-top: 63px; +} +.left-arrow { + width: 0; + height: 0; + margin-right: 20px; + border-style: solid; + border-width: 12px 18px 12px 0; + border-color: transparent #424242 transparent transparent; +} +.right-arrow { + margin-left: 20px; + width: 0; + height: 0; + border-style: solid; + border-width: 12px 0 12px 18px; + border-color: transparent transparent transparent #424242; +} + +.revision { + width: 1%; + margin-top: 75px; +} + +.stopper { + position: absolute; + top: 50%; + margin-left: -10px; + height: 50px; + width: 0; +} + +.slider { + position: relative; +} +.revision-slider { + position: relative; + width: 100%; + margin: 0 auto; +} + +.pointer-container { + position: absolute; + clear: both; + width: 100%; + top: 77px; + z-index: 10; +} +.left-pointer { + border-color: transparent transparent #00f transparent; +} +.right-pointer { + border-color: transparent transparent #f00 transparent; +} +.pointer { + position: absolute !important; + z-index: 11; + width: 0; + height: 0; + border-style: solid; + border-width: 0 10px 10px 10px; +} diff --git a/modules/ext.RevisionSlider.fetchRevisions.js b/modules/ext.RevisionSlider.fetchRevisions.js new file mode 100644 index 0000000..933cf23 --- /dev/null +++ b/modules/ext.RevisionSlider.fetchRevisions.js @@ -0,0 +1,26 @@ +( function ( mw, $ ) { + mw.libs.revisionSlider = mw.libs.revisionSlider || {}; + + /** + * Fetches up to 500 revisions at a time + * + * @param {{}} options - Options containing success callback, pageName and startId + */ + mw.libs.revisionSlider.fetchRevisions = function ( options ) { + $.ajax( { + url: mw.util.wikiScript( 'api' ), + data: { + action: 'query', + prop: 'revisions', + format: 'json', + rvprop: 'ids|timestamp|user|comment|parsedcomment|size|flags', + titles: options.pageName, + formatversion: 2, + rvstartid: options.startId, + 'continue': '', + rvlimit: 500 + }, + success: options.success + } ); + }; +}( mediaWiki, jQuery ) ); diff --git a/modules/ext.RevisionSlider.init.js b/modules/ext.RevisionSlider.init.js new file mode 100755 index 0000000..bc13c64 --- /dev/null +++ b/modules/ext.RevisionSlider.init.js @@ -0,0 +1,281 @@ +( function ( mw, $ ) { + var revisionWidth = 10, + $container = null, + $revisionSlider = null, + revs = [], + pointerPosL = -1, + pointerPosR = -1; + + // Function called when a tick on the slider is clicked + // Params: v1 - Left revision ID; v2 - Right revision ID + // function refresh( v1, v2 ) { + // if( v1 === -1 || v2 === -1 ) return; + // + // var $url = gServer + gScript + '?title=' + gPageName + '&diff=' + v2 + '&oldid=' + v1; + // location.href = $url; + // } + + // Formating date in JS + function formatDate( rawDate ) { + var months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec' ], + f = new Date( rawDate ), + fDate = f.getUTCDate(), + fMonth = f.getUTCMonth(), + fYear = f.getUTCFullYear(), + fHours = ( '0' + f.getUTCHours()).slice( -2 ), + fMinutes = ( '0' + f.getUTCMinutes()).slice( -2 ); + + return ( fHours + ':' + fMinutes + ', ' + fDate + ' ' + months[ fMonth ] + ' ' + fYear ).toString(); + } + + function getSection( text ) { + text = text.match( + new RegExp( '(/\\* [^\\*]* \\*/)', 'gi' ) + ); + if ( !text ) { + return ''; + } + return text[ 0 ].replace( + new RegExp( ' \\*/|/\\* ', 'ig' ), + '' + ); + } + + function getComposedRevData( revs ) { + var max = 0, + changeSize = 0, + section, + sectionMap = {}, + result; + + for ( var i = 1; i < revs.length; i++ ) { + changeSize = Math.abs( revs[ i ].size - revs[ i - 1 ].size ); + section = getSection( revs[ i ].comment ); + if ( changeSize > max ) { + max = changeSize; + } + if ( section.length > 0 && !(section in sectionMap) ) { + sectionMap[ section ] = ''; + } + } + + i = 0; + for ( var sectionName in sectionMap ) { + sectionMap[ sectionName ] = mw.libs.revisionSlider.rainbow( Object.keys( sectionMap ).length, i ); + i++; + } + + result = { + maxChangeSize: max, + sectionMap: sectionMap + }; + + return result; + } + + // Setting the tick marks on the slider + // Params: element - jQuery slider; revs - revisions data from API + function setSliderTicks( element, revs ) { + var $slider = $( element ), + revData = getComposedRevData( revs ), + maxChangeSizeLogged = Math.log( revData.maxChangeSize ); + + for ( var i = 1; i < revs.length; i++ ) { + var diffSize = revs[ i ].size - revs[ i - 1 ].size, + relativeChangeSize = Math.ceil( 65.0 * Math.log( Math.abs( diffSize ) ) / maxChangeSizeLogged ) + 5, + section = getSection( revs[ i ].comment ), + html = '<b>' + formatDate( revs[ i ].timestamp ) + '</b><br>'; + + html += mw.html.escape( revs[ i ].user ) + '<br>'; + if ( revs[ i ].comment !== '' ) { + html += '<br><i>' + mw.html.escape( revs[ i ].parsedcomment ) + '</i>'; + } + //html += '<br> ' + revs[i].minor ? '<b>K</b> ' : ''; + html += '<br>' + diffSize + ' byte'; + $( '<div class="ui-slider-tick-mark revision" title="<center>' + html + '</center>"/>' ) + .css( { + 'left': i + '%', + 'height': relativeChangeSize + 'px', + 'top': diffSize > 0 ? '-' + relativeChangeSize + 'px' : 0, + 'background': revData.sectionMap[ section ] || 'black' + //'opacity' : revs[i].minor ? 0.15 : 0.35 + //'background' : i%2 == 1 ? 'white' : 'black' + } ) + .tipsy( { + gravity: 's', + html: true, + fade: true + } ) + .appendTo( $slider ); + $( '<div class="stopper"/>' ) + .css( 'left', (i - 1) + '.5%' ) + .appendTo( $slider ); + } + } + + /** + * Checks whether pointerPos is between start and end + * + * @param {int} pointerPos + * @param {int} start + * @param {int} end + * @returns {boolean} + */ + function isPointerInRange( pointerPos, start, end ) { + return pointerPos >= start && + pointerPos <= Math.min( revs.length, end ); + } + + /** + * Slowly slides/scrolls $container one viewport in a given direction + * + * @param {jQuery} $container + * @param {int} direction + */ + function slide( $container, direction ) { + $container.animate( { + scrollLeft: $container.scrollLeft() + ($container.width() * direction) + } ); + } + + /** + * Determines the revision from a position in the revisionsContainer + * + * @param {int} pos + * @returns {number} + */ + function revisionOfPosition( pos ) { + return Math.floor( pos / revisionWidth ); + } + + /** + * Slides a $pointer to a position + * + * @param {jQuery} $pointer + * @param {int} pos + */ + function slideToPosition( $pointer, pos ) { + var containerOffset = $container.offset().left - $revisionSlider.offset().left, + left = (pos % 100) * revisionWidth; + + $pointer.animate( { left: left + containerOffset } ); + } + + /** + * Slides a $pointer to the side of the slider + * + * @param {jQuery} $pointer + * @param {int} pointerPos + * @param {int} direction + */ + function slideToSide( $pointer, pointerPos, direction ) { + var containerOffset = $revisionSlider.find( '.arrow' ).outerWidth() + 20, // 20 == margin right + isLeft = pointerPos < revisionOfPosition( $container.scrollLeft() ) + direction * revisionOfPosition( $container.width() ), + sideFactor = isLeft ? -1 : 1, + sideOffset = 3 * revisionWidth * sideFactor / 2, + offsetRight = $pointer.hasClass( 'left-pointer' ) ? -revisionWidth : 0, + xPos = isLeft ? containerOffset : $container.width() + containerOffset; + + $pointer.animate( { left: xPos + offsetRight + sideOffset } ); + } + + function getSectionLegend( revs ) { + var revData = getComposedRevData( revs ), + html = ''; + for ( var sectionName in revData.sectionMap ) { + html += '<span class="rvslider-legend-box" style="color:' + revData.sectionMap[ sectionName ] + ';"> ■</span>' + sectionName + ''; + } + return html; + } + + function addSlider( revs ) { + var $revisions = $( '<div class="revisions"></div>' ).css( 'width', revs.length * revisionWidth ), + $leftPointer = $( '<div class="pointer left-pointer" />' ), + $rightPointer = $( '<div class="pointer right-pointer" />' ); + + $revisionSlider = $( '<div class="revision-slider" />' ) + .append( $( '<a class="arrow left-arrow" data-dir="-1"></a>' ) ) + .append( $( '<div class="revisions-container" />' ).append( $revisions ) ) + .append( $( '<a class="arrow right-arrow" data-dir="1"></a>' ) ) + .append( $( '<div style="clear: both" />' ) ) + .append( + $( '<div class="pointer-container" />' ) + .append( $leftPointer ) + .append( $rightPointer ) + ); + + $container = $revisionSlider.find( '.revisions-container' ); + + $revisionSlider.find( '.arrow' ).click( function () { + var direction = $( this ).data( 'dir' ), + newStart = revisionOfPosition( + Math.min( $container.find( '.revisions' ).width() - $container.width(), Math.max( 0, $container.scrollLeft() ) ) + ) + ( direction * revisionOfPosition( $container.width() ) ), + newEnd = newStart + revisionOfPosition( $container.width() ); + + if ( isPointerInRange( pointerPosL, newStart, newEnd ) ) { + slideToPosition( $leftPointer, pointerPosL ); + } else { + slideToSide( $leftPointer, pointerPosL, direction ); + } + + if ( isPointerInRange( pointerPosR, newStart, newEnd ) ) { + slideToPosition( $rightPointer, pointerPosR ); + } else { + slideToSide( $rightPointer, pointerPosR, direction ); + } + + slide( $container, direction ); + } ); + + setSliderTicks( $revisions, revs ); + + $revisionSlider.find( '.pointer' ).draggable( { + axis: 'x', + snap: '.stopper', + containment: '.revisions-container', + stop: function () { + var posLeft = parseInt( $( this ).css( 'left' ), 10 ), + offset = $revisionSlider.find( '.arrow' ).outerWidth() + 20, + pos = Math.round( (posLeft + $container.scrollLeft() - offset) / revisionWidth ); + + if ( $( this ).hasClass( 'left-pointer' ) ) { + pointerPosL = pos; + } else { + pointerPosR = pos; + } + + // refresh( pointerPosL, pointerPosR ); + } + } ); + + var $html = $( '<td colspan="4" style="text-align:center;" class="slider"></td>' ).append( $revisionSlider ); + var $html2 = $( '<tr>' ).append( $html ); + var $legendHtml = $( '<td colspan="4" style="text-align:center; font-size: 0.75em; padding: 1em 0 0.5em;"></td>' ).append( getSectionLegend( revs ) ); + $( '.diff > tbody > tr' ).eq( 0 ).after( $legendHtml ).after( $html2 ); + + revisionWidth = $( '.slider' ).width() * 90 / 10000; + slideToSide( $leftPointer, -1, 1 ); + slideToSide( $rightPointer, -1, 1 ); + } + + mw.loader.using( [ 'jquery.ui.draggable', 'jquery.ui.tooltip', 'jquery.tipsy' ], function () { + $( function () { + mw.libs.revisionSlider.fetchRevisions( { + pageName: mw.config.get( 'wgPageName' ), + startId: mw.config.get( 'wgCurRevisionId' ), + + success: function ( data ) { + revs = data.query.pages[ 0 ].revisions; + if ( !revs ) { + return; + } + revs.reverse(); + + addSlider( revs ); + } + } ); + } ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/modules/ext.RevisionSlider.rainbow.js b/modules/ext.RevisionSlider.rainbow.js new file mode 100644 index 0000000..f71304e --- /dev/null +++ b/modules/ext.RevisionSlider.rainbow.js @@ -0,0 +1,52 @@ +( function ( mw ) { + mw.libs.revisionSlider = mw.libs.revisionSlider || {}; + + // see http://stackoverflow.com/a/7419630/4782503 + mw.libs.revisionSlider.rainbow = function ( numOfSteps, step ) { + // This function generates vibrant, "evenly spaced" colours (i.e. no clustering). This is ideal for creating easily distinguishable vibrant markers in Google Maps and other apps. + // Adam Cole, 2011-Sept-14 + // HSV to RBG adapted from: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript + var r, g, b, + c, + h = step / numOfSteps, + i = Math.floor( h * 6 ), + f = h * 6 - i, + q = 1 - f; + + switch ( i % 6 ) { + case 0: + r = 1; + g = f; + b = 0; + break; + case 1: + r = q; + g = 1; + b = 0; + break; + case 2: + r = 0; + g = 1; + b = f; + break; + case 3: + r = 0; + g = q; + b = 1; + break; + case 4: + r = f; + g = 0; + b = 1; + break; + case 5: + r = 1; + g = 0; + b = q; + break; + } + c = '#' + ( '00' + Math.floor( r * 255 ).toString( 16 ) ).slice( -2 ) + ( '00' + Math.floor( g * 255 ).toString( 16 ) ).slice( -2 ) + ( '00' + Math.floor( b * 255 ).toString( 16 ) ).slice( -2 ); + + return c; + }; +}( mediaWiki ) ); diff --git a/package.json b/package.json new file mode 100755 index 0000000..d64fe9b --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "RevisionSlider", + "version": "0.0.0", + "devDependencies": { + "grunt": "0.4.5", + "grunt-cli": "0.1.13", + "grunt-contrib-jshint": "0.11.3", + "grunt-banana-checker": "0.4.0", + "grunt-jscs": "2.1.0", + "grunt-jsonlint": "1.0.4" + } +} \ No newline at end of file diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..e12d0ec --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<ruleset> + <rule ref="vendor/mediawiki/mediawiki-codesniffer/MediaWiki"/> + <file>.</file> + <arg name="extensions" value="php,php5,inc"/> + <arg name="encoding" value="utf8"/> + <exclude-pattern>vendor</exclude-pattern> +</ruleset> diff --git a/tests/RevisionSlider.test.js b/tests/RevisionSlider.test.js new file mode 100755 index 0000000..34dcf99 --- /dev/null +++ b/tests/RevisionSlider.test.js @@ -0,0 +1,12 @@ +( function ( mw, $ ) { + QUnit.module( 'ext.revisionSlider' ); + + /** + * Write your QUnit tests here. For more information on + * how to write proper JavaScript QUnit tests for + * MediaWiki extension development, please read + * the manual: + * https://www.mediawiki.org/wiki/Manual:JavaScript_unit_testing#Write_a_unit_test + */ + +} )( mediaWiki, jQuery ); diff --git a/tests/RevisionSlider.test.php b/tests/RevisionSlider.test.php new file mode 100755 index 0000000..b7a9013 --- /dev/null +++ b/tests/RevisionSlider.test.php @@ -0,0 +1,7 @@ +<?php + +/** + * For more information on how to create PHPUnit tests + * for your extension, visit the documentation page: + * https://www.mediawiki.org/wiki/Manual:PHP_unit_testing/Writing_unit_tests + */ -- To view, visit https://gerrit.wikimedia.org/r/284173 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I2e22365f3b93a76d5b8d3997242b5fed996c6d78 Gerrit-PatchSet: 5 Gerrit-Project: mediawiki/extensions/RevisionSlider Gerrit-Branch: master Gerrit-Owner: Jakob <jakob.warkot...@wikimedia.de> Gerrit-Reviewer: Addshore <addshorew...@gmail.com> Gerrit-Reviewer: Legoktm <legoktm.wikipe...@gmail.com> Gerrit-Reviewer: Siebrand <siebr...@kitano.nl> Gerrit-Reviewer: Tobias Gritschacher <tobias.gritschac...@wikimedia.de> Gerrit-Reviewer: WMDE-Fisch <christoph.fisc...@wikimedia.de> Gerrit-Reviewer: WMDE-leszek <leszek.mani...@wikimedia.de> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits