Bug 1290173 - Introduce Narrate word tracking test. r?mikedeboer draft
authorEitan Isaacson <eitan@monotonous.org>
Thu, 29 Sep 2016 13:28:30 -0700
changeset 419602 9db94003aa478040c66ef694869ae14b5a1f0682
parent 419601 75725bdf6e56477c224090928e5cfcd811088b67
child 532609 5db8a9252ebcc93150864b8eea9d432be57621a2
push id30967
push userbmo:eitan@monotonous.org
push dateFri, 30 Sep 2016 16:57:36 +0000
Bug 1290173 - Introduce Narrate word tracking test. r?mikedeboer Added a sample article in the Narrate module for testing special cases. MozReview-Commit-ID: 1oSem9LARB3
--- a/toolkit/components/narrate/Narrator.jsm
+++ b/toolkit/components/narrate/Narrator.jsm
@@ -162,29 +162,48 @@ Narrator.prototype = {
     } else {
       utterance.lang = this._speechOptions.lang;
     this._startTime = Date.now();
     let highlighter = new Highlighter(paragraph);
+    if (this._inTest) {
+      let onTestSynthEvent = e => {
+        if (e.detail.type == "boundary") {
+          let args = Object.assign({ utterance }, e.detail.args);
+          let evt = new this._win.SpeechSynthesisEvent(e.detail.type, args);
+          utterance.dispatchEvent(evt);
+        }
+      };
+      let removeListeners = () => {
+        this._win.removeEventListener("testsynthevent", onTestSynthEvent);
+      };
+      this._win.addEventListener("testsynthevent", onTestSynthEvent);
+      utterance.addEventListener("end", removeListeners);
+      utterance.addEventListener("error", removeListeners);
+    }
     return new Promise((resolve, reject) => {
       utterance.addEventListener("start", () => {
         let bb = paragraph.getBoundingClientRect();
         if (bb.top < 0 || bb.bottom > this._win.innerHeight) {
           paragraph.scrollIntoView({ behavior: "smooth", block: "start"});
         if (this._inTest) {
           this._sendTestEvent("paragraphstart", {
             voice: utterance.chosenVoiceURI,
             rate: utterance.rate,
-            paragraph: paragraph.textContent
+            paragraph: paragraph.textContent,
+            tag: paragraph.localName
       utterance.addEventListener("end", () => {
         if (!this._win) {
           // page got unloaded, don't do anything.
@@ -219,16 +238,22 @@ Narrator.prototype = {
         // Match non-whitespace. This isn't perfect, but the most universal
         // solution for now.
         let reWordBoundary = /\S+/g;
         // Match the first word from the boundary event offset.
         reWordBoundary.lastIndex = e.charIndex;
         let firstIndex = reWordBoundary.exec(paragraph.textContent);
         if (firstIndex) {
           highlighter.highlight(firstIndex.index, reWordBoundary.lastIndex);
+          if (this._inTest) {
+            this._sendTestEvent("wordhighlight", {
+              start: firstIndex.index,
+              end: reWordBoundary.lastIndex
+            });
+          }
   start: function(speechOptions) {
--- a/toolkit/components/narrate/test/NarrateTestUtils.jsm
+++ b/toolkit/components/narrate/test/NarrateTestUtils.jsm
@@ -119,10 +119,31 @@ this.NarrateTestUtils = {
     return new Promise(resolve => {
       function observeChange() {
         Services.prefs.removeObserver(pref, observeChange);
       Services.prefs.addObserver(pref, observeChange, false);
+  },
+  sendBoundaryEvent: function(window, name, charIndex) {
+    let detail = { type: "boundary", args: { name, charIndex } };
+    window.dispatchEvent(new window.CustomEvent("testsynthevent",
+      { detail: detail }));
+  },
+  isWordHighlightGone: function(window, ok) {
+    let $ = window.document.querySelector.bind(window.document);
+    ok(!$(".narrate-word-highlight"), "No more word highlights exist");
+  },
+  getWordHighlights: function(window) {
+    let $$ = window.document.querySelectorAll.bind(window.document);
+    let nodes = Array.from($$(".narrate-word-highlight"));
+    return nodes.map(node => {
+      return { word: node.dataset.word,
+               left: Number(node.style.left.replace(/px$/, "")),
+               top: Number(node.style.top.replace(/px$/, ""))};
+    });
--- a/toolkit/components/narrate/test/browser.ini
+++ b/toolkit/components/narrate/test/browser.ini
@@ -1,9 +1,10 @@
 support-files =
-  !/browser/base/content/test/general/readerModeArticle.html
+  moby_dick.html
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/test/browser_word_highlight.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* globals is, isnot, registerCleanupFunction, add_task */
+"use strict";
+add_task(function* testNarrate() {
+  setup("urn:moz-tts:fake-indirect:teresa");
+  yield spawnInNewReaderTab(TEST_ARTICLE, function* () {
+    let $ = content.document.querySelector.bind(content.document);
+    let popup = $(NarrateTestUtils.POPUP);
+    ok(!NarrateTestUtils.isVisible(popup), "popup is initially hidden");
+    let toggle = $(NarrateTestUtils.TOGGLE);
+    toggle.click();
+    ok(NarrateTestUtils.isVisible(popup), "popup toggled");
+    NarrateTestUtils.isStoppedState(content, ok);
+    let promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+    $(NarrateTestUtils.START).click();
+    let voice = (yield promiseEvent).detail.voice;
+    is(voice, "urn:moz-tts:fake-indirect:teresa", "double-check voice");
+    // Skip forward to first paragraph.
+    let details;
+    do {
+      promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+      $(NarrateTestUtils.FORWARD).click();
+      details = (yield promiseEvent).detail;
+    } while (details.tag != "p");
+    let boundaryPat = /(\s+)\S/g;
+    let position = { left: 0, top: 0 };
+    let text = details.paragraph;
+    for (let res = boundaryPat.exec(text); res; res = boundaryPat.exec(text)) {
+      promiseEvent = ContentTaskUtils.waitForEvent(content, "wordhighlight");
+      NarrateTestUtils.sendBoundaryEvent(content, "word", res.index);
+      let { start, end } = (yield promiseEvent).detail;
+      let nodes = NarrateTestUtils.getWordHighlights(content);
+      for (let node of nodes) {
+        // Since this is English we can assume each word is to the right or
+        // below the previous one.
+        ok(node.left > position.left || node.top > position.top,
+          "highlight position is moving");
+        position = { left: node.left, top: node.top };
+      }
+      let wordFromOffset = text.substring(start, end);
+      // XXX: Each node should contain the part of the word it highlights.
+      // Right now, each node contains the entire word.
+      let wordFromHighlight = nodes[0].word;
+      is(wordFromOffset, wordFromHighlight, "Correct word is highlighted");
+    }
+    $(NarrateTestUtils.STOP).click();
+    yield ContentTaskUtils.waitForCondition(
+      () => !$(NarrateTestUtils.STOP), "transitioned to stopped state");
+    NarrateTestUtils.isWordHighlightGone(content, ok);
+  });
--- a/toolkit/components/narrate/test/head.js
+++ b/toolkit/components/narrate/test/head.js
@@ -2,18 +2,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 /* exported teardown, setup, toggleExtension,
    spawnInNewReaderTab, TEST_ARTICLE  */
 "use strict";
-const TEST_ARTICLE = "http://example.com/browser/browser/base/content/test/" +
-  "general/readerModeArticle.html";
+  "http://example.com/browser/toolkit/components/narrate/test/moby_dick.html";
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
@@ -22,21 +22,25 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 const TEST_PREFS = [
   ["reader.parse-on-load.enabled", true],
   ["media.webspeech.synth.enabled", true],
   ["media.webspeech.synth.test", true],
   ["narrate.enabled", true],
   ["narrate.test", true]
-function setup() {
+function setup(voiceUri) {
   // Set required test prefs.
   TEST_PREFS.forEach(([name, value]) => {
     setBoolPref(name, value);
+  if (voiceUri) {
+    Services.prefs.setCharPref("narrate.voice", voiceUri);
+  }
 function teardown() {
   // Reset test prefs.
   TEST_PREFS.forEach(pref => {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/test/moby_dick.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<title>Moby Dick - Chapter 1. Loomings</title>
+  <h1>Moby Dick</h1>
+  <h2>Chapter 1. Loomings</h2>
+  <p>
+    Call me Ishmael. <span>Some <span>years</span></span> ago—never mind how
+    long precisely—having little or no money in my purse, and nothing particular
+    to interest me on shore, I thought I would sail about a little and see the
+    watery part of the world. It is a way I have of driving off the spleen and
+    regulating the circulation. Whenever I find myself growing grim about the
+    mouth; whenever it is a damp, drizzly November in my soul; whenever I find
+    myself involuntarily pausing before coffin warehouses, and bringing up the
+    rear of every funeral I meet; and especially whenever my hypos get such an
+    upper hand of me, that it requires a strong moral principle to prevent me
+    from deliberately stepping into the street, and methodically knocking
+    people's hats off—then, I account it high time to get to sea as soon as I
+    can. This is my substitute for pistol and ball. With a philosophical
+    flourish Cato throws himself upon his sword; I quietly take to the ship.
+    There is nothing surprising in this. If they but knew it, almost all men in
+    their degree, some time or other, cherish very nearly the same feelings
+    towards the ocean with me.
+  </p>
+  <p>
+    There now is your insular city of the Manhattoes, belted round by wharves
+    as Indian isles by coral reefs—commerce surrounds it with her surf.
+    Right and left, the streets take you waterward. Its extreme downtown is
+    the battery, where that noble mole is washed by waves, and cooled by
+    breezes, which a few hours previous were out of sight of land. Look at the
+    crowds of water-gazers there.
+  </p>
+  <p>
+    Circumambulate the city of a dreamy Sabbath afternoon. Go from Corlears
+    Hook to Coenties Slip, and from thence, by Whitehall, northward. What do
+    you see?—Posted like silent sentinels all around the town, stand
+    thousands upon thousands of mortal men fixed in ocean reveries. Some
+    leaning against the spiles; some seated upon the pier-heads; some looking
+    over the bulwarks of ships from China; some high aloft in the rigging, as
+    if striving to get a still better seaward peep. But these are all
+    landsmen; of week days pent up in lath and plaster—tied to counters,
+    nailed to benches, clinched to desks. How then is this? Are the green
+    fields gone? What do they here?
+  </p>
+  <p>
+    But look! here come more crowds, pacing straight for the water, and
+    seemingly bound for a dive. Strange! Nothing will content them but the
+    extremest limit of the land; loitering under the shady lee of yonder
+    warehouses will not suffice. No. They must get just as nigh the water as
+    they possibly can without falling in. And there they stand—miles of
+    them—leagues. Inlanders all, they come from lanes and alleys,
+    streets and avenues—north, east, south, and west. Yet here they all
+    unite. Tell me, does the magnetic virtue of the needles of the compasses
+    of all those ships attract them thither?
+  </p>
+  <p>
+    Once more. Say you are in the country; in some high land of lakes. Take
+    almost any path you please, and ten to one it carries you down in a dale,
+    and leaves you there by a pool in the stream. There is magic in it. Let
+    the most absent-minded of men be plunged in his deepest reveries—stand
+    that man on his legs, set his feet a-going, and he will infallibly lead
+    you to water, if water there be in all that region. Should you ever be
+    athirst in the great American desert, try this experiment, if your caravan
+    happen to be supplied with a metaphysical professor. Yes, as every one
+    knows, meditation and water are wedded for ever.
+  </p>
+  <p>
+    But here is an artist. He desires to paint you the dreamiest, shadiest,
+    quietest, most enchanting bit of romantic landscape in all the valley of
+    the Saco. What is the chief element he employs? There stand his trees,
+    each with a hollow trunk, as if a hermit and a crucifix were within; and
+    here sleeps his meadow, and there sleep his cattle; and up from yonder
+    cottage goes a sleepy smoke. Deep into distant woodlands winds a mazy way,
+    reaching to overlapping spurs of mountains bathed in their hill-side blue.
+    But though the picture lies thus tranced, and though this pine-tree shakes
+    down its sighs like leaves upon this shepherd's head, yet all were vain,
+    unless the shepherd's eye were fixed upon the magic stream before him. Go
+    visit the Prairies in June, when for scores on scores of miles you wade
+    knee-deep among Tiger-lilies—what is the one charm wanting?—Water—there
+    is not a drop of water there! Were Niagara but a cataract of sand, would
+    you travel your thousand miles to see it? Why did the poor poet of
+    Tennessee, upon suddenly receiving two handfuls of silver, deliberate
+    whether to buy him a coat, which he sadly needed, or invest his money in a
+    pedestrian trip to Rockaway Beach? Why is almost every robust healthy boy
+    with a robust healthy soul in him, at some time or other crazy to go to
+    sea? Why upon your first voyage as a passenger, did you yourself feel such
+    a mystical vibration, when first told that you and your ship were now out
+    of sight of land? Why did the old Persians hold the sea holy? Why did the
+    Greeks give it a separate deity, and own brother of Jove? Surely all this
+    is not without meaning. And still deeper the meaning of that story of
+    Narcissus, who because he could not grasp the tormenting, mild image he
+    saw in the fountain, plunged into it and was drowned. But that same image,
+    we ourselves see in all rivers and oceans. It is the image of the
+    ungraspable phantom of life; and this is the key to it all.
+  </p>
+  <p>
+    Now, when I say that I am in the habit of going to sea whenever I begin to
+    grow hazy about the eyes, and begin to be over conscious of my lungs, I do
+    not mean to have it inferred that I ever go to sea as a passenger. For to
+    go as a passenger you must needs have a purse, and a purse is but a rag
+    unless you have something in it. Besides, passengers get sea-sick—grow
+    quarrelsome—don't sleep of nights—do not enjoy themselves
+    much, as a general thing;—no, I never go as a passenger; nor, though
+    I am something of a salt, do I ever go to sea as a Commodore, or a
+    Captain, or a Cook. I abandon the glory and distinction of such offices to
+    those who like them. For my part, I abominate all honourable respectable
+    toils, trials, and tribulations of every kind whatsoever. It is quite as
+    much as I can do to take care of myself, without taking care of ships,
+    barques, brigs, schooners, and what not. And as for going as cook,—though
+    I confess there is considerable glory in that, a cook being a sort of
+    officer on ship-board—yet, somehow, I never fancied broiling fowls;—though
+    once broiled, judiciously buttered, and judgmatically salted and peppered,
+    there is no one who will speak more respectfully, not to say
+    reverentially, of a broiled fowl than I will. It is out of the idolatrous
+    dotings of the old Egyptians upon broiled ibis and roasted river horse,
+    that you see the mummies of those creatures in their huge bake-houses the
+    pyramids.
+  </p>
+  <p>
+    No, when I go to sea, I go as a simple sailor, right before the mast,
+    plumb down into the forecastle, aloft there to the royal mast-head. True,
+    they rather order me about some, and make me jump from spar to spar, like
+    a grasshopper in a May meadow. And at first, this sort of thing is
+    unpleasant enough. It touches one's sense of honour, particularly if you
+    come of an old established family in the land, the Van Rensselaers, or
+    Randolphs, or Hardicanutes. And more than all, if just previous to putting
+    your hand into the tar-pot, you have been lording it as a country
+    schoolmaster, making the tallest boys stand in awe of you. The transition
+    is a keen one, I assure you, from a schoolmaster to a sailor, and requires
+    a strong decoction of Seneca and the Stoics to enable you to grin and bear
+    it. But even this wears off in time.
+  </p>
+  <p>
+    What of it, if some old hunks of a sea-captain orders me to get a broom
+    and sweep down the decks? What does that indignity amount to, weighed, I
+    mean, in the scales of the New Testament? Do you think the archangel
+    Gabriel thinks anything the less of me, because I promptly and
+    respectfully obey that old hunks in that particular instance? Who ain't a
+    slave? Tell me that. Well, then, however the old sea-captains may order me
+    about—however they may thump and punch me about, I have the
+    satisfaction of knowing that it is all right; that everybody else is one
+    way or other served in much the same way—either in a physical or
+    metaphysical point of view, that is; and so the universal thump is passed
+    round, and all hands should rub each other's shoulder-blades, and be
+    content.
+  </p>
+  <p>
+    Again, I always go to sea as a sailor, because they make a point of paying
+    me for my trouble, whereas they never pay passengers a single penny that I
+    ever heard of. On the contrary, passengers themselves must pay. And there
+    is all the difference in the world between paying and being paid. The act
+    of paying is perhaps the most uncomfortable infliction that the two
+    orchard thieves entailed upon us. But <i>being paid</i>,—what will compare
+    with it? The urbane activity with which a man receives money is really
+    marvellous, considering that we so earnestly believe money to be the root
+    of all earthly ills, and that on no account can a monied man enter heaven.
+    Ah! how cheerfully we consign ourselves to perdition!
+  </p>
+  <p>
+    Finally, I always go to sea as a sailor, because of the wholesome exercise
+    and pure air of the fore-castle deck. For as in this world, head winds are
+    far more prevalent than winds from astern (that is, if you never violate
+    the Pythagorean maxim), so for the most part the Commodore on the
+    quarter-deck gets his atmosphere at second hand from the sailors on the
+    forecastle. He thinks he breathes it first; but not so. In much the same
+    way do the commonalty lead their leaders in many other things, at the same
+    time that the leaders little suspect it. But wherefore it was that after
+    having repeatedly smelt the sea as a merchant sailor, I should now take it
+    into my head to go on a whaling voyage; this the invisible police officer
+    of the Fates, who has the constant surveillance of me, and secretly dogs
+    me, and influences me in some unaccountable way—he can better answer
+    than any one else. And, doubtless, my going on this whaling voyage, formed
+    part of the grand programme of Providence that was drawn up a long time
+    ago. It came in as a sort of brief interlude and solo between more
+    extensive performances. I take it that this part of the bill must have run
+    something like this:
+  </p>
+  <p>
+    "<i>Grand Contested Election for the Presidency of the United States.</i>
+  </p>
+  <p>
+    Though I cannot tell why it was exactly that those stage managers, the
+    Fates, put me down for this shabby part of a whaling voyage, when others
+    were set down for magnificent parts in high tragedies, and short and easy
+    parts in genteel comedies, and jolly parts in farces—though I cannot
+    tell why this was exactly; yet, now that I recall all the circumstances, I
+    think I can see a little into the springs and motives which being
+    cunningly presented to me under various disguises, induced me to set about
+    performing the part I did, besides cajoling me into the delusion that it
+    was a choice resulting from my own unbiased freewill and discriminating
+    judgment.
+  </p>
+  <p>
+    Chief among these motives was the overwhelming idea of the great whale
+    himself. Such a portentous and mysterious monster roused all my curiosity.
+    Then the wild and distant seas where he rolled his island bulk; the
+    undeliverable, nameless perils of the whale; these, with all the attending
+    marvels of a thousand Patagonian sights and sounds, helped to sway me to
+    my wish. With other men, perhaps, such things would not have been
+    inducements; but as for me, I am tormented with an everlasting itch for
+    things remote. I love to sail forbidden seas, and land on barbarous
+    coasts. Not ignoring what is good, I am quick to perceive a horror, and
+    could still be social with it—would they let me—since it is
+    but well to be on friendly terms with all the inmates of the place one
+    lodges in.
+  </p>
+  <p>
+    By reason of these things, then, the whaling voyage was welcome; the great
+    flood-gates of the wonder-world swung open, and in the wild conceits that
+    swayed me to my purpose, two and two there floated into my inmost soul,
+    endless processions of the whale, and, mid most of them all, one grand
+    hooded phantom, like a snow hill in the air.
+  </p>