jenkins-bot has submitted this change and it was merged.

Change subject: Start and stop recitation with mouse
......................................................................


Start and stop recitation with mouse

Added functionality for starting and stopping recitation by clicking a button
on the page. This is prepared by adding utterance tags to the page, containing
the utterance as a string and an audio tag to use for playing the resulting
audio. When an uttrance is played, the utterance string is sent to the TTS
server. The URL for the resulting audio file is retrieved from the response
and set as source for the audio element.

Updated PHP code to run with PHP 5.5.

Bug: T133672
Change-Id: Ie040eaa780576f16ed4cb1c1be6d223c99540fc1
---
M .jshintrc
M Gruntfile.js
M Hooks.php
M composer.json
M extension.json
M includes/Cleaner.php
A includes/HtmlGenerator.php
M includes/Segmenter.php
M modules/ext.wikispeech.css
M modules/ext.wikispeech.js
M package.json
D tests/Wikispeech.test.js
D tests/Wikispeech.test.php
M tests/phpunit/CleanerTest.php
A tests/phpunit/HtmlGeneratorTest.php
A tests/qunit/ext.wikispeech.test.js
16 files changed, 738 insertions(+), 152 deletions(-)

Approvals:
  Lokal Profil: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/.jshintrc b/.jshintrc
index 5cbb855..95facb2 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -2,7 +2,9 @@
        "predef": [
                "mediaWiki",
                "jQuery",
-               "QUnit"
+               "QUnit",
+               "sinon",
+               "JSON"
        ],
 
        // Enforcing
diff --git a/Gruntfile.js b/Gruntfile.js
index 9da849a..f67b264 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -13,7 +13,8 @@
                        },
                        all: [
                                '*.js',
-                               'modules/**/*.js'
+                               'modules/**/*.js',
+                               'tests/**/*.js'
                        ]
                },
                jscs: {
@@ -29,6 +30,14 @@
                }
        } );
 
-       grunt.registerTask( 'test', [ 'jshint', 'jscs', 'jsonlint', 'banana' ] 
);
+       grunt.registerTask(
+               'test',
+               [
+                       'jshint',
+                       'jscs',
+                       'jsonlint',
+                       'banana'
+               ]
+       );
        grunt.registerTask( 'default', 'test' );
 };
diff --git a/Hooks.php b/Hooks.php
index 17d68f5..7d98a87 100644
--- a/Hooks.php
+++ b/Hooks.php
@@ -6,22 +6,29 @@
  * @license GPL-2.0+
  */
 
+require_once __DIR__ . '/includes/Cleaner.php';
+require_once __DIR__ . '/includes/Segmenter.php';
+require_once __DIR__ . '/includes/HtmlGenerator.php';
+
 class WikispeechHooks {
+
        /**
-        * Conditionally register the unit testing module for the 
ext.wikispeech module
-        * only if that module is loaded
+        * Conditionally register the unit testing module for the ext.wikispeech
+        * module only if that module is loaded
         *
         * @param array $testModules The array of registered test modules
-        * @param ResourceLoader $resourceLoader The reference to the resource 
loader
+        * @param ResourceLoader $resourceLoader The reference to the resource
+        *      loader
         * @return true
         */
+
        public static function onResourceLoaderTestModules(
                array &$testModules,
                ResourceLoader &$resourceLoader
        ) {
                $testModules['qunit']['ext.wikispeech.tests'] = [
                        'scripts' => [
-                               'tests/Wikispeech.test.js'
+                               'tests/qunit/ext.wikispeech.test.js'
                        ],
                        'dependencies' => [
                                'ext.wikispeech'
@@ -31,4 +38,55 @@
                ];
                return true;
        }
+
+       /**
+        * Hook for ParserAfterTidy.
+        *
+        * Adds Wikispeech elements to the HTML, if the page is in the main
+        * namespace.
+        *
+        * @param $parser Parser object. Can be used to manually parse a portion
+        *      of wiki text from the $text.
+        * @param $text Represents the text for page.
+        */
+
+       public static function onParserAfterTidy( &$parser, &$text ) {
+               if ( $parser->getTitle()->getNamespace() == NS_MAIN && $text != 
"" ) {
+                       wfDebugLog(
+                               'Wikispeech',
+                               'HTML from onParserAfterTidy(): ' . $text
+                       );
+                       $cleanedText = Cleaner::cleanHtml( $text );
+                       wfDebugLog( 'Wikispeech', 'Cleaned text: ' . 
$cleanedText );
+                       $utterances = Segmenter::segmentSentences( $cleanedText 
);
+                       wfDebugLog(
+                               'Wikispeech',
+                               'Utterances: ' . var_export( $utterances, true )
+                       );
+                       $utterancesHtml =
+                               HtmlGenerator::generateUtterancesHtml( 
$utterances );
+                       wfDebugLog(
+                               'Wikispeech',
+                               'Adding utterances HTML: ' . $utterancesHtml
+                       );
+                       $text .= $utterancesHtml;
+               }
+       }
+
+       /**
+        * Hook for BeforePageDisplay.
+        *
+        * Enables JavaScript.
+        *
+        * @param OutputPage $out The OutputPage object.
+        * @param Skin $skin Skin object that will be used to generate the page,
+        *      added in 1.13.
+        */
+
+       public static function onBeforePageDisplay(
+               OutputPage &$out,
+               Skin &$skin
+       ) {
+               $out->addModules( [ 'ext.wikispeech' ] );
+       }
 }
diff --git a/composer.json b/composer.json
index 8a29463..d7f4e86 100644
--- a/composer.json
+++ b/composer.json
@@ -6,7 +6,8 @@
        "scripts": {
                "test": [
                        "parallel-lint . --exclude vendor",
-                       "phpcs -p -s"
+                       "phpcs -p -s",
+                       "grunt test"
                ],
                "fix": [
                        "phpcbf"
diff --git a/extension.json b/extension.json
index 31fc973..2d3a525 100644
--- a/extension.json
+++ b/extension.json
@@ -1,53 +1,59 @@
 {
-    "name": "Wikispeech",
-    "version": "0.0.1",
-    "author": [
-        "Sebastian Berlin"
-    ],
-    "url": "https://www.mediawiki.org/wiki/Extension:Wikispeech";,
-    "namemsg": "wikispeech",
-    "descriptionmsg": "wikispeech-desc",
-    "license-name": "GPL-2.0+",
-    "type": "other",
-    "manifest_version": 1,
-    "MessagesDirs": {
-        "Wikispeech": [
-            "i18n"
-        ]
-    },
-    "AutoloadClasses": {
-        "SpecialWikispeech": "specials/SpecialWikispeech.php",
-        "WikispeechHooks": "Hooks.php"
-    },
-    "ResourceModules": {
-        "ext.wikispeech": {
-            "scripts": [
-                "ext.wikispeech.js"
-            ],
-            "styles": [
-                "ext.wikispeech.css"
-            ],
-            "messages": [
+       "name": "Wikispeech",
+       "version": "0.0.1",
+       "author": [
+               "Sebastian Berlin"
+       ],
+       "url": "https://www.mediawiki.org/wiki/Extension:Wikispeech";,
+       "namemsg": "wikispeech",
+       "descriptionmsg": "wikispeech-desc",
+       "license-name": "GPL-2.0+",
+       "type": "other",
+       "manifest_version": 1,
+       "MessagesDirs": {
+               "Wikispeech": [
+                       "i18n"
+               ]
+       },
+       "AutoloadClasses": {
+               "SpecialWikispeech": "specials/SpecialWikispeech.php",
+               "WikispeechHooks": "Hooks.php"
+       },
+       "ResourceModules": {
+               "ext.wikispeech": {
+                       "scripts": [
+                               "ext.wikispeech.js"
+                       ],
+                       "styles": [
+                               "ext.wikispeech.css"
+                       ],
+                       "messages": [
 
-            ],
-            "dependencies": [
+                       ],
+                       "dependencies": [
 
-            ]
-        }
-    },
-    "ResourceFileModulePaths": {
-        "localBasePath": "modules",
-        "remoteExtPath": "Wikispeech/modules"
-    },
-    "SpecialPages": {
-        "Wikispeech": "SpecialWikispeech"
-    },
-    "ExtensionMessagesFiles": {
-        "WikispeechAlias": "Wikispeech.alias.php"
-    },
-    "Hooks": {
-        "ResourceLoaderTestModules": [
-            "WikispeechHooks::onResourceLoaderTestModules"
-        ]
-    }
+                       ]
+               }
+       },
+       "ResourceFileModulePaths": {
+               "localBasePath": "modules",
+               "remoteExtPath": "Wikispeech/modules"
+       },
+       "SpecialPages": {
+               "Wikispeech": "SpecialWikispeech"
+       },
+       "ExtensionMessagesFiles": {
+               "WikispeechAlias": "Wikispeech.alias.php"
+       },
+       "Hooks": {
+               "ResourceLoaderTestModules": [
+                       "WikispeechHooks::onResourceLoaderTestModules"
+               ],
+               "ParserAfterTidy": [
+                       "WikispeechHooks::onParserAfterTidy"
+               ],
+               "BeforePageDisplay": [
+                       "WikispeechHooks::onBeforePageDisplay"
+               ]
+       }
 }
diff --git a/includes/Cleaner.php b/includes/Cleaner.php
index 8f8361a..10e318a 100644
--- a/includes/Cleaner.php
+++ b/includes/Cleaner.php
@@ -37,9 +37,11 @@
                // @codingStandardsIgnoreStart
                $wrappedText = '<head><meta http-equiv="Content-Type" 
content="text/html; charset=utf-8"/><dummy>' . $markedUpText . 
'</dummy></head>';
                // @codingStandardsIgnoreEnd
+               libxml_use_internal_errors( true );
                $dom->loadHTML(
                        $wrappedText,
-                       LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED );
+                       LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED
+               );
                $cleanedText = self::getTextContent( $dom->documentElement );
                return $cleanedText;
        }
diff --git a/includes/HtmlGenerator.php b/includes/HtmlGenerator.php
new file mode 100644
index 0000000..6d94604
--- /dev/null
+++ b/includes/HtmlGenerator.php
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * @file
+ * @ingroup Extensions
+ * @license GPL-2.0+
+ */
+
+class HtmlGenerator {
+
+       /**
+        * Generate an HTML string for a sequence of utternaces. Utterance tags
+        * look like this:
+        * <utterance id="utterance-0><text>Utterance 
string.</text><audio></audio></utterance>
+        * The <text> and <audio> tags are used to request audio from the TTS
+        * server and store the response.
+        *
+        * @since 0.0.1
+        * @param array $utterances The utterance strings to generate HTML from.
+        * @return string An HTML string containing the <utterance> tags, 
wrapped
+        *      in an <utterances> tag.
+        */
+
+       public static function generateUtterancesHtml( $utterances ) {
+               if ( count( $utterances ) ) {
+                       $dom = new DOMDocument();
+                       $utterancesNode = $dom->createElement( 'utterances' );
+                       // Hide the content of the utterance elements.
+                       $utterancesNode->setAttribute( 'hidden', '' );
+                       $index = 0;
+                       foreach ( $utterances as $utteranceString ) {
+                               $utteranceNode = self::generateUtteranceElement(
+                                       $dom,
+                                       $utteranceString,
+                                       $index
+                               );
+                               $utterancesNode->appendChild( $utteranceNode );
+                               $index += 1;
+                       }
+                       $utternacesHtml = urldecode( $dom->saveHTML( 
$utterancesNode ) );
+                       return $utternacesHtml;
+               }
+       }
+
+       /**
+        * Create an utterance element, which has child elements for the 
utterance
+        * string and audio.
+        *
+        * @since 0.0.1
+        * @param DOMDocument $dom The DOMDocument to use for creating the
+        *      elements.
+        * @param string $utteranceString The string to add to the text element,
+        *      which is later sent to the TTS server.
+        * @param int $index The index of the element, used for giving it an id.
+        *      Later used for playing the utterances in the correct order.
+        * @return DOMElement The resulting utterance element.
+        */
+
+       private static function generateUtteranceElement(
+               $dom,
+               $utteranceString,
+               $index
+       ) {
+               $utteranceElement = $dom->createElement( 'utterance' );
+               $utteranceElement->setAttribute( 'id', "utterance-$index" );
+               $textNode = $dom->createElement(
+                       'text',
+                       // URL encoding (and later decoding) if required due to
+                       // strings containing # not being written otherwise.
+                       urlencode( $utteranceString ) );
+               $utteranceElement->appendChild( $textNode );
+               $audioNode = $dom->createElement( 'audio' );
+               $utteranceElement->appendChild( $audioNode );
+               return $utteranceElement;
+       }
+}
diff --git a/includes/Segmenter.php b/includes/Segmenter.php
index 1b2ab1f..f9af191 100644
--- a/includes/Segmenter.php
+++ b/includes/Segmenter.php
@@ -18,7 +18,7 @@
         * @return array The segments found.
         */
 
-       public function segmentSentences( $text ) {
+       public static function segmentSentences( $text ) {
                $matches = [];
                // Find the indices of all characters that may be sentence 
final.
                preg_match_all(
@@ -57,10 +57,16 @@
         * @return bool True if the character is sentence final, else false.
         */
 
-       private function isSentenceFinal( $string, $index ) {
+       private static function isSentenceFinal( $string, $index ) {
                $character = $string[ $index ];
-               $nextCharacter = $string[ $index + 1 ];
-               $characterAfterNext = $string[ $index + 2 ];
+               $nextCharacter = null;
+               if ( strlen( $string ) > $index + 1 ) {
+                       $nextCharacter = $string[ $index + 1 ];
+               }
+               $characterAfterNext = null;
+               if ( strlen( $string ) > $index + 2 ) {
+                       $characterAfterNext = $string[ $index + 2 ];
+               }
                if ( $character == "\n" ) {
                        // A newline is always sentence final.
                        return true;
@@ -86,7 +92,7 @@
         * @return bool True if the entire string is upper case, else false.
         */
 
-       private function isUpper( $string ) {
+       private static function isUpper( $string ) {
                return mb_strtoupper( $string, 'UTF-8' ) == $string;
        }
 
@@ -99,7 +105,7 @@
         * are discarded.
         */
 
-       public function segmentParagraphs( $text ) {
+       public static function segmentParagraphs( $text ) {
                $segments = [];
                foreach ( explode( "\n", $text ) as $segment ) {
                        if ( strlen( trim( $segment ) ) > 0 ) {
diff --git a/modules/ext.wikispeech.css b/modules/ext.wikispeech.css
index 2c65c32..896e735 100644
--- a/modules/ext.wikispeech.css
+++ b/modules/ext.wikispeech.css
@@ -1,3 +1,11 @@
 .wikispeech-title {
        font-size: 1.1em;
 }
+
+.ext-wikispeech-play:after {
+       content: "Play";
+}
+
+.ext-wikispeech-stop:after {
+       content: "Stop";
+}
diff --git a/modules/ext.wikispeech.js b/modules/ext.wikispeech.js
index 3a8bd04..c456d48 100644
--- a/modules/ext.wikispeech.js
+++ b/modules/ext.wikispeech.js
@@ -1,8 +1,208 @@
-( function () {
-       /**
-        * @class mw.wikispeech
-        * @singleton
-        */
-       mw.wikispeech = {
-       };
-}() );
+( function ( mw, $ ) {
+       function Wikispeech() {
+               var self, $currentUtterance;
+
+               self = this;
+               $currentUtterance = $();
+
+               /**
+                * Adds button for starting and stopping recitation to the page.
+                *
+                * When no utterance is playing, clicking starts the first 
utterance.
+                * When an utterance is being played, clicking stops the 
playback.
+                * The button changes appearance to reflect its current 
function.
+                */
+
+               this.addPlayStopButton = function () {
+                       var $playStopButton;
+
+                       $playStopButton = $( '<button></button>' )
+                               .attr( 'id', 'ext-wikispeech-play-stop-button' )
+                               .addClass( 'ext-wikispeech-play' );
+                       $( '#firstHeading' ).append( $playStopButton );
+                       $playStopButton.click( function () {
+                               if ( $currentUtterance.length === 0 ) {
+                                       self.play();
+                               } else {
+                                       self.stop();
+                               }
+                       } );
+               };
+
+               /**
+                * Start playing the first utterance.
+                */
+
+               this.play = function () {
+                       var $playStopButton;
+
+                       $currentUtterance = $( '#utterance-0 ' );
+                       $currentUtterance.children( 'audio' ).trigger( 'play' );
+                       $playStopButton = $( '#ext-wikispeech-play-stop-button' 
);
+                       $playStopButton.removeClass( 'ext-wikispeech-play' );
+                       $playStopButton.addClass( 'ext-wikispeech-stop' );
+               };
+
+               /**
+                * Stop playing the utterance currently playing.
+                */
+
+               this.stop = function () {
+                       var $playStopButton;
+
+                       self.stopUtterance( $currentUtterance );
+                       $currentUtterance = $();
+                       $playStopButton = $( '#ext-wikispeech-play-stop-button' 
);
+                       $playStopButton.removeClass( 'ext-wikispeech-stop' );
+                       $playStopButton.addClass( 'ext-wikispeech-play' );
+               };
+
+               /**
+                * Stop and rewind the audio for an utterance.
+                *
+                * @param $utterance The utterance to stop the audio for.
+                */
+
+               this.stopUtterance = function ( $utterance ) {
+                       $utterance.children( 'audio' ).trigger( 'pause' );
+                       // Rewind audio for next time it plays.
+                       $utterance.children( 'audio' ).prop( 'currentTime', 0 );
+               };
+
+               /**
+                * Prepare an utterance for playback.
+                *
+                * Audio for the utterance is requested from the TTS server and 
event
+                * listeners are added. When an utterance starts playing, the 
next one
+                * is prepared, and when an utterance is done, the next 
utterance is
+                * played. This is meant to be a balance between not having to 
pause
+                * between utterance and not requesting more than needed.
+
+                * @param $utterance The utterance to prepare.
+                */
+
+               this.prepareUtterance = function ( $utterance ) {
+                       var $audio, $nextUtterance, $nextUtteranceAudio;
+
+                       if ( !$utterance.prop( 'requested' ) ) {
+                               // Only load audio for an utterance if we 
haven't already
+                               // sent a request for it.
+                               self.loadAudio( $utterance );
+                               $nextUtterance = self.getNextUtterance( 
$utterance );
+                               $audio = $utterance.children( 'audio' );
+                               if ( $nextUtterance.length === 0 ) {
+                                       // For last utterance, just stop the 
playback when done.
+                                       $audio.on( 'ended', function () {
+                                               self.stop();
+                                       } );
+                               } else {
+                                       $nextUtteranceAudio = 
$nextUtterance.children( 'audio' );
+                                       $audio.on( {
+                                               play: function () {
+                                                       $currentUtterance = 
$utterance;
+                                                       self.prepareUtterance( 
$nextUtterance );
+                                               },
+                                               ended: function () {
+                                                       
$nextUtteranceAudio.trigger( 'play' );
+                                               }
+                                       } );
+                               }
+                       }
+               };
+
+               /**
+                * Get the utterance after the given utterance.
+                *
+                * @param $utterance The original utterance.
+                * @return The utterance after the original utterance.
+                */
+
+               this.getNextUtterance = function ( $utterance ) {
+                       var utteranceIdParts, nextUtteranceIndex, 
nextUtteranceId;
+
+                       // Utterance id's follow the pattern "utterance-x", 
where x is
+                       // the index.
+                       utteranceIdParts = $utterance.attr( 'id' ).split( '-' );
+                       nextUtteranceIndex = parseInt( utteranceIdParts[ 1 ], 
10 ) + 1;
+                       utteranceIdParts[ 1 ] = nextUtteranceIndex;
+                       nextUtteranceId = utteranceIdParts.join( '-' );
+                       return $( '#' + nextUtteranceId );
+               };
+
+               /**
+                * Request audio for an utterance.
+                *
+                * When the response is received, set the audio URL as the 
source for
+                * the utterance's audio element.
+                *
+                * @param $utterance The utterance to load audio for.
+                */
+
+               this.loadAudio = function ( $utterance ) {
+                       var $audio, text, audioUrl;
+
+                       $audio = $utterance.children( 'audio' );
+                       mw.log( 'Loading audio for: ' + $utterance.attr( 'id' ) 
);
+                       text = $utterance.children( 'text' ).text();
+                       self.requestTts( text, function ( response ) {
+                               audioUrl = response.audio;
+                               mw.log( 'Setting url for ' + $utterance.attr( 
'id' ) + ': ' +
+                                               audioUrl );
+                               $audio.attr( 'src', audioUrl );
+                       } );
+                       $utterance.prop( 'requested', true );
+               };
+
+               /**
+                * Send a request to the TTS server.
+                *
+                * The request should specify the following parameters:
+                * - lang: the language used by the synthesizer.
+                * - input_type: "ssml" if you want SSML markup, otherwise 
"text" for
+                * plain text.
+                * - input: the text to be synthesized.
+                * For more on the parameters, see:
+                * https://github.com/stts-se/wikispeech_mockup/wiki/api.
+                *
+                * @param {string} text The utterance string to send in the 
request.
+                * @param {Function} callback Function to be called when a 
response
+                *      is received.
+                */
+
+               this.requestTts = function ( text, callback ) {
+                       var request, parameters, url, response;
+
+                       request = new XMLHttpRequest();
+                       request.overrideMimeType( 'text/json' );
+                       url = 'https://morf.se/wikispeech/';
+                       request.open( 'POST', url, true );
+                       request.setRequestHeader(
+                               'Content-type',
+                               'application/x-www-form-urlencoded'
+                       );
+                       parameters = $.param( {
+                               // jscs:disable 
requireCamelCaseOrUpperCaseIdentifiers
+                               lang: 'en',
+                               input_type: 'text',
+                               input: text
+                               // jscs:enable 
requireCamelCaseOrUpperCaseIdentifiers
+                       } );
+                       request.onload = function () {
+                               response = JSON.parse( request.responseText );
+                               callback( response );
+                       };
+                       mw.log( 'Sending request: ' + url + '?' + parameters );
+                       request.send( parameters );
+               };
+       }
+
+       mw.wikispeech = {};
+       mw.wikispeech.Wikispeech = Wikispeech;
+
+       if ( $( 'utterances' ).length ) {
+               mw.wikispeech.wikispeech = new mw.wikispeech.Wikispeech();
+               // Prepare the first utterance for playback.
+               mw.wikispeech.wikispeech.prepareUtterance( $( '#utterance-0' ) 
);
+               mw.wikispeech.wikispeech.addPlayStopButton();
+       }
+}( mediaWiki, jQuery ) );
diff --git a/package.json b/package.json
index ea896d0..d90753b 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,16 @@
 {
-  "name": "Wikispeech",
-  "version": "0.0.1",
-  "scripts": {
-    "test": "grunt test"
-  },
-  "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
+       "name": "Wikispeech",
+       "version": "0.0.1",
+       "scripts": {
+               "test": "grunt test"
+       },
+       "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",
+               "qunitjs": "1.23.1"
+       }
+}
diff --git a/tests/Wikispeech.test.js b/tests/Wikispeech.test.js
deleted file mode 100644
index 5e5f3ad..0000000
--- a/tests/Wikispeech.test.js
+++ /dev/null
@@ -1,12 +0,0 @@
-( function () {
-       QUnit.module( 'ext.wikispeech' );
-
-       /**
-        * 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/Wikispeech.test.php b/tests/Wikispeech.test.php
deleted file mode 100644
index b7a9013..0000000
--- a/tests/Wikispeech.test.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?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
- */
diff --git a/tests/phpunit/CleanerTest.php b/tests/phpunit/CleanerTest.php
index 9b77b77..d1b78ab 100644
--- a/tests/phpunit/CleanerTest.php
+++ b/tests/phpunit/CleanerTest.php
@@ -12,10 +12,7 @@
        public function testCleanTags() {
                $markedUpText = '<i>Blonde on Blonde</i>';
                $expectedText = 'Blonde on Blonde';
-               $this->assertTextCleaned(
-                       'Cleaner::cleanHtml',
-                       $expectedText,
-                       $markedUpText );
+               $this->assertTextCleaned( $expectedText, $markedUpText );
        }
 
        /**
@@ -27,7 +24,6 @@
         * should not be altered.
         *
         * @since 0.0.1
-        * @param string $function Name of the function to test.
         * @param string $expectedText The string that is the expected output
         * from the function named by $function.
         * @param string $markedUpText The string that contains the markup
@@ -35,33 +31,29 @@
         * $function.
         */
 
-       private function assertTextCleaned( $function, $expectedText,
-               $markedUpText ) {
-               $this->assertEquals( $expectedText, $function( $markedUpText ) 
);
+       private function assertTextCleaned( $expectedText, $markedUpText ) {
+               $this->assertEquals(
+                       $expectedText,
+                       Cleaner::cleanHtml( $markedUpText )
+               );
                $this->assertEquals( 'prefix' . $expectedText . 'suffix',
-                       $function( 'prefix' . $markedUpText . 'suffix' ) );
+                       Cleaner::cleanHtml( 'prefix' . $markedUpText . 'suffix' 
) );
                $this->assertEquals( $expectedText . 'infix' . $expectedText,
-                       $function( $markedUpText . 'infix' . $markedUpText ) );
+                       Cleaner::cleanHtml( $markedUpText . 'infix' . 
$markedUpText ) );
                $this->assertEquals( 'A string without any fancy markup.',
-                       $function( 'A string without any fancy markup.' ) );
+                       Cleaner::cleanHtml( 'A string without any fancy 
markup.' ) );
        }
 
        public function testCleanNestedTags() {
                $markedUpText = '<i><b>Blonde on Blonde</b></i>';
                $expectedText = 'Blonde on Blonde';
-               $this->assertTextCleaned(
-                       'Cleaner::cleanHtml',
-                       $expectedText,
-                       $markedUpText );
+               $this->assertTextCleaned( $expectedText, $markedUpText );
        }
 
        public function testCleanEmptyTags() {
                $markedUpText = '<img alt="" src="image.png" />';
                $expectedText = '';
-               $this->assertTextCleaned(
-                       'Cleaner::cleanHtml',
-                       $expectedText,
-                       $markedUpText );
+               $this->assertTextCleaned( $expectedText, $markedUpText );
        }
 
        public function testRemoveTagsAltogether() {
@@ -69,19 +61,13 @@
                $markedUpText = '<table>Remove this table, please.</table>';
                // @codingStandardsIgnoreEnd
                $expectedText = '';
-               $this->assertTextCleaned(
-                       'Cleaner::cleanHtml',
-                       $expectedText,
-                       $markedUpText );
+               $this->assertTextCleaned( $expectedText, $markedUpText );
        }
 
        public function testRemoveTagsWithCertainClass() {
                $markedUpText = '<sup class="reference"><a>[1]</a>Also remove 
this.</sup>';
                $expectedText = '';
-               $this->assertTextCleaned(
-                       'Cleaner::cleanHtml',
-                       $expectedText,
-                       $markedUpText );
+               $this->assertTextCleaned( $expectedText, $markedUpText );
        }
 
        public function testDontRemoveTagsWithoutCertainClass() {
@@ -89,10 +75,7 @@
                $markedUpText = '<sup>I am not a reference.</sup><sup 
class="not-a-reference">Neither am I.</sup>';
                // @codingStandardsIgnoreEnd
                $expectedText = 'I am not a reference.Neither am I.';
-               $this->assertTextCleaned(
-                       'Cleaner::cleanHtml',
-                       $expectedText,
-                       $markedUpText );
+               $this->assertTextCleaned( $expectedText, $markedUpText );
        }
 
        public function testHandleMultipleClasses() {
@@ -100,10 +83,7 @@
                $markedUpText = '<sup class="reference another-class"><a 
href="#cite_note-Grayp5-1">[1]</a>Also remove this.</sup>';
                // @codingStandardsIgnoreEnd
                $expectedText = '';
-               $this->assertTextCleaned(
-                       'Cleaner::cleanHtml',
-                       $expectedText,
-                       $markedUpText );
+               $this->assertTextCleaned( $expectedText, $markedUpText );
        }
 
        public function testCleanNestedTagsWhereSomeAreRemovedAndSomeAreKept() {
@@ -111,27 +91,18 @@
                $markedUpText = '<h2><span class="mw-headline" 
id="Recording_sessions">Recording sessions</span><mw:editsection page="Test 
Page" section="1">Recording sessions</mw:editsection></h2>';
                // @codingStandardsIgnoreEnd
                $expectedText = 'Recording sessions';
-               $this->assertTextCleaned(
-                       'Cleaner::cleanHtml',
-                       $expectedText,
-                       $markedUpText );
+               $this->assertTextCleaned( $expectedText, $markedUpText );
        }
 
        public function testHandleUtf8Characters() {
                $markedUpText = '—';
                $expectedText = '—';
-               $this->assertTextCleaned(
-                       'Cleaner::cleanHtml',
-                       $expectedText,
-                       $markedUpText );
+               $this->assertTextCleaned( $expectedText, $markedUpText );
        }
 
        public function testHandleHtmlEntities() {
                $markedUpText = '6&#160;p.m';
                $expectedText = '6 p.m';
-               $this->assertTextCleaned(
-                       'Cleaner::cleanHtml',
-                       $expectedText,
-                       $markedUpText );
+               $this->assertTextCleaned( $expectedText, $markedUpText );
        }
 }
diff --git a/tests/phpunit/HtmlGeneratorTest.php 
b/tests/phpunit/HtmlGeneratorTest.php
new file mode 100644
index 0000000..6f26234
--- /dev/null
+++ b/tests/phpunit/HtmlGeneratorTest.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * @ingroup Extensions
+ * @license GPL-2.0+
+ */
+
+require_once __DIR__ . '/../../includes/HtmlGenerator.php';
+
+class HtmlGeneratorTest extends MediaWikiTestCase {
+       public function testGenerateUtterancesHtml() {
+               $utterancesStrings = [ 'An utterance.', 'Another utterance.' ];
+               $actualHtml = HtmlGenerator::generateUtterancesHtml(
+                       $utterancesStrings
+               );
+               // @codingStandardsIgnoreStart
+               $expectedHtml = '<utterances hidden=""><utterance 
id="utterance-0"><text>An 
utterance.</text><audio></audio></utterance><utterance 
id="utterance-1"><text>Another 
utterance.</text><audio></audio></utterance></utterances>';
+               // @codingStandardsIgnoreEnd
+               $this->assertEquals( $expectedHtml, $actualHtml );
+       }
+
+       public function testGenerateUtteranceContainingNumberSign() {
+               // @codingStandardsIgnoreStart
+               $utterancesStrings = [ 'Blonde on Blonde spawned two singles 
that were top-twenty hits in the US: "Rainy Day Women #12 & 35" and "I Want 
You".'
+               ];
+               // @codingStandardsIgnoreEnd
+               $actualHtml = HtmlGenerator::generateUtterancesHtml(
+                       $utterancesStrings
+               );
+               // @codingStandardsIgnoreStart
+               $expectedHtml = '<utterances hidden=""><utterance 
id="utterance-0"><text>Blonde on Blonde spawned two singles that were 
top-twenty hits in the US: "Rainy Day Women #12 & 35" and "I Want 
You".</text><audio></audio></utterance></utterances>';
+               // @codingStandardsIgnoreEnd
+               $this->assertEquals( $expectedHtml, $actualHtml );
+       }
+
+       public function testDontGenerateUtterancesHtmlForNoUtterances() {
+               $utterancesStrings = [];
+               $actualHtml = HtmlGenerator::generateUtterancesHtml(
+                       $utterancesStrings
+               );
+               $expectedHtml = '';
+               $this->assertEquals( $expectedHtml, $actualHtml );
+       }
+}
diff --git a/tests/qunit/ext.wikispeech.test.js 
b/tests/qunit/ext.wikispeech.test.js
new file mode 100644
index 0000000..43cffa3
--- /dev/null
+++ b/tests/qunit/ext.wikispeech.test.js
@@ -0,0 +1,220 @@
+( function ( mw, $ ) {
+       var wikispeech, server;
+
+       QUnit.module( 'ext.wikispeech', {
+               setup: function () {
+                       wikispeech = new mw.wikispeech.Wikispeech();
+                       server = sinon.fakeServer.create();
+                       server.respondWith( '{"audio": 
"http://server.url/audio"}' );
+                       // overrideMimeType() isn't defined by default.
+                       server.xhr.prototype.overrideMimeType = function () {};
+                       $( '#qunit-fixture' ).append( createUtteranceElement(
+                               'utterance-0',
+                               'A mockup utterance.'
+                       ) );
+                       $( '#qunit-fixture' ).append( createUtteranceElement(
+                               'utterance-1',
+                               'Another mockup utterance.'
+                       ) );
+                       $( '#qunit-fixture' ).append(
+                               $( '<h1></h1>' ).attr( 'id', 'firstHeading' )
+                       );
+               },
+               teardown: function () {
+                       server.restore();
+               }
+       } );
+
+       function createUtteranceElement( id, text ) {
+               return $( '<utterance></utterance>' )
+                       .attr( 'id', id )
+                       .append( $( '<text></text>' )
+                               .text( text ) )
+                       .append( $( '<audio></audio>' ) );
+       }
+
+       QUnit.test( 'prepareUtterance', function ( assert ) {
+               assert.expect( 1 );
+               sinon.spy( wikispeech, 'loadAudio' );
+
+               wikispeech.prepareUtterance( $( '#utterance-0' ) );
+
+               assert.strictEqual(
+                       wikispeech.loadAudio.calledWith( $( '#utterance-0' ) ),
+                       true
+               );
+       } );
+
+       // jscs:disable validateQuoteMarks
+       QUnit.test( "prepareUtterance: don't request if already requested", 
function ( assert ) {
+               // jscs:enable validateQuoteMarks
+               assert.expect( 1 );
+               sinon.spy( wikispeech, 'loadAudio' );
+               $( '#utterance-0' ).prop( 'requested', true );
+
+               wikispeech.prepareUtterance( $( '#utterance-0' ) );
+
+               assert.strictEqual( wikispeech.loadAudio.called, false );
+       } );
+
+       QUnit.test( 'prepareUtterance: prepare next utterance when playing', 
function ( assert ) {
+               var $nextUtterance;
+
+               assert.expect( 1 );
+               wikispeech.prepareUtterance( $( '#utterance-0' ) );
+               $nextUtterance = $( '#utterance-1' );
+               sinon.spy( wikispeech, 'prepareUtterance' );
+
+               $( '#utterance-0 audio' ).trigger( 'play' );
+
+               assert.strictEqual(
+                       wikispeech.prepareUtterance.calledWith( $nextUtterance 
),
+                       true
+               );
+       } );
+
+       // jscs:disable validateQuoteMarks
+       QUnit.test( "prepareUtterance: don't prepare next audio if it doesn't 
exist", function ( assert ) {
+               // jscs:enable validateQuoteMarks
+               assert.expect( 1 );
+               sinon.spy( wikispeech, 'prepareUtterance' );
+               wikispeech.prepareUtterance( $( '#utterance-1' ) );
+
+               $( '#utterance-1 audio' ).trigger( 'play' );
+
+               assert.strictEqual( wikispeech.prepareUtterance.calledWith(
+                       $( '#utterance-2' ) ), false );
+       } );
+
+       QUnit.test( 'prepareUtterance: play next utterance when ended', 
function ( assert ) {
+               var $nextUtterance;
+
+               assert.expect( 1 );
+               // Assume that both utterances are prepared.
+               wikispeech.prepareUtterance( $( '#utterance-0' ) );
+               $nextUtterance = $( '#utterance-1' );
+               wikispeech.prepareUtterance( $nextUtterance );
+               sinon.spy( $( '#utterance-1 audio' ).get( 0 ), 'play' );
+
+               $( '#utterance-0 audio' ).trigger( 'ended' );
+
+               assert.strictEqual(
+                       $nextUtterance.children( 'audio' ).get( 0 ).play.called,
+                       true
+               );
+       } );
+
+       QUnit.test( 'prepareUtterance: stop when end of text is reached', 
function ( assert ) {
+               var $lastUtterance;
+
+               assert.expect( 1 );
+               sinon.spy( wikispeech, 'stop' );
+               $lastUtterance = $( '#utterance-1' );
+               wikispeech.prepareUtterance( $lastUtterance );
+
+               $lastUtterance.children( 'audio' ).trigger( 'ended' );
+
+               assert.strictEqual( wikispeech.stop.called, true );
+       } );
+
+       QUnit.test( 'loadAudio', function ( assert ) {
+               assert.expect( 3 );
+
+               wikispeech.loadAudio( $( '#utterance-0' ) );
+
+               server.respond();
+               assert.strictEqual(
+                       server.requests[ 0 ].requestBody,
+                       'lang=en&input_type=text&input=A+mockup+utterance.'
+               );
+               assert.strictEqual(
+                       $( '#utterance-0 audio' ).attr( 'src' ),
+                       'http://server.url/audio'
+               );
+               assert.strictEqual(
+                       $( '#utterance-0' ).prop( 'requested' ),
+                       true
+               );
+       } );
+
+       QUnit.test( 'addPlayStopButton', function ( assert ) {
+               assert.expect( 1 );
+               wikispeech.addPlayStopButton();
+
+               assert.strictEqual(
+                       $( '#firstHeading #ext-wikispeech-play-stop-button' 
).length,
+                       1
+               );
+       } );
+
+       QUnit.test( 'addPlayStopButton: play', function ( assert ) {
+               assert.expect( 1 );
+               wikispeech.addPlayStopButton();
+               sinon.spy( wikispeech, 'play' );
+
+               $( '#ext-wikispeech-play-stop-button' ).trigger( 'click' );
+
+               assert.strictEqual( wikispeech.play.called, true );
+       } );
+
+       QUnit.test( 'addPlayStopButton: stop', function ( assert ) {
+               assert.expect( 1 );
+               wikispeech.addPlayStopButton();
+               wikispeech.play();
+               sinon.spy( wikispeech, 'stop' );
+
+               $( '#ext-wikispeech-play-stop-button' ).trigger( 'click' );
+
+               assert.strictEqual( wikispeech.stop.called, true );
+       } );
+
+       QUnit.test( 'stop', function ( assert ) {
+               assert.expect( 4 );
+               wikispeech.addPlayStopButton();
+               wikispeech.play();
+               $( '#utterance-0 audio' ).prop( 'currentTime', 1 );
+
+               wikispeech.stop();
+
+               assert.strictEqual( $( '#utterance-0 audio' ).prop( 'paused' ), 
true );
+               assert.strictEqual(
+                       $( '#utterance-0 audio' ).prop( 'currentTime' ),
+                       0
+               );
+               assert.strictEqual(
+                       $( '#ext-wikispeech-play-stop-button' )
+                               .hasClass( 'ext-wikispeech-play' ),
+                       true
+               );
+               assert.strictEqual(
+                       $( '#ext-wikispeech-play-stop-button' )
+                               .hasClass( 'ext-wikispeech-stop' ),
+                       false
+               );
+       } );
+
+       QUnit.test( 'play', function ( assert ) {
+               var $firstUtterance;
+
+               assert.expect( 3 );
+               wikispeech.addPlayStopButton();
+               $firstUtterance = $( '#utterance-0' );
+
+               wikispeech.play();
+
+               assert.strictEqual(
+                       $firstUtterance.children( 'audio' ).prop( 'paused' ),
+                       false
+               );
+               assert.strictEqual(
+                       $( '#ext-wikispeech-play-stop-button' )
+                               .hasClass( 'ext-wikispeech-stop' ),
+                       true
+               );
+               assert.strictEqual(
+                       $( '#ext-wikispeech-play-stop-button' )
+                               .hasClass( 'ext-wikispeech-play' ),
+                       false
+               );
+       } );
+} )( mediaWiki, jQuery );

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Ie040eaa780576f16ed4cb1c1be6d223c99540fc1
Gerrit-PatchSet: 10
Gerrit-Project: mediawiki/extensions/Wikispeech
Gerrit-Branch: master
Gerrit-Owner: Sebastian Berlin (WMSE) <sebastian.ber...@wikimedia.se>
Gerrit-Reviewer: Lokal Profil <lokal.pro...@gmail.com>
Gerrit-Reviewer: Sebastian Berlin (WMSE) <sebastian.ber...@wikimedia.se>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to