<commit_before>/* 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/. */

const EXPORTED_SYMBOLS = ['HighlightManager'];

function HighlightManager(opts) {
  /*
   The Highlight Manager abstracts out the task of adjusting the blue highlight
   in the autocomplete popup in response to key and mouse events, as well as
   in response to the recommendation being shown.
   */
  this.win = opts.win;
  this.events = opts.events;
  this.popup = opts.popup;
  // this is a recommendation view, because the highlight
  // class manages passing focus between it and the list of results
  this.recommendation = opts.recommendation;

  this.adjustHighlight = this.adjustHighlight.bind(this);
  this.clearRecommendationHighlight = this.clearRecommendationHighlight.bind(this);
  this.initMutationObserver = this.initMutationObserver.bind(this);
  this.mutationHandler = this.mutationHandler.bind(this);
  this.onRecommendationMouseEnter = this.onRecommendationMouseEnter.bind(this);
  this.onRecommendationMouseLeave = this.onRecommendationMouseLeave.bind(this);
  this.onRecommendationMouseMove = this.onRecommendationMouseMove.bind(this);
  this.stealHighlight = this.stealHighlight.bind(this);
}

HighlightManager.prototype = {
  init: function() {
    this.events.subscribe('navigational-key', this.adjustHighlight);
    // It seems that the recommendation is rarely in the XUL DOM when this init
    // function runs, so listen for the recommendation to be found in the DOM,
    // then try a second time to attach the MutationObserver to the element.
    this.events.subscribe('recommendation-created', this.initMutationObserver);
    this.events.subscribe('recommendation-shown', this.stealHighlight);

    this.attachMouseListeners();
    this.initMutationObserver();
  },
  destroy: function() {
    this.resultsObserver.disconnect();
    this.detachMouseListeners();

    this.events.unsubscribe('recommendation-created', this.initMutationObserver);
    this.events.unsubscribe('recommendation-shown', this.stealHighlight);
    this.events.unsubscribe('navigational-key', this.adjustHighlight);

    delete this.popup;
    delete this.recommendation;
    delete this.win;
  },
  attachMouseListeners: function() {
    // This function is important because it gathers docs for all the mouse
    // event handlers in a single place.

    // When the user mouses into the recommendation, the results list might
    // have had the focus, so swap the highlight. No need to resort to stealing
    // the highlight, because there's no race to update the DOM.
    this.events.subscribe('recommendation-mouseenter', this.onRecommendationMouseEnter);

    // The mouseenter listener fails to apply the highlight to the
    // recommendation in edge cases where the highlight is applied to the
    // results list when the mouse pointer is inside the recommendation. When
    // the user starts moving the mouse, no mouseenter event is fired, because
    // the mouse pointer was already inside the element. This most often
    // happens when the stealHighlight() method fails to steal the highlight
    // (stealHighlight is non-deterministic), but can also reproducibly occur
    // in flows like this example:
    // 1. User mouses over the recommendation, highlighting it
    // 2. User keys away from the recommendation, highlighting a result in the
    //    results list
    // 3. User wiggles the mouse inside the recommendation: no mouseenter is
    //    fired, so no highlight is applied without a mousemove listener.
    //
    // While this is an edge case, the listener is simple, and makes the mouse
    // highlight management code very robust against timing errors.
    this.events.subscribe('recommendation-mousemove', this.onRecommendationMouseMove);

    // When the user mouses out of the recommendation, if the mouse is leaving
    // the popup, keep the recommendation highlighted, matching the mouseout
    // behavior of the results list. If the mouse is moving to the results
    // list, remove the highlight from the recommendation.
    this.events.subscribe('recommendation-mouseleave', this.onRecommendationMouseLeave);

    // When the user mouses over the results list, the recommendation may have
    // the highlight, and should lose the highlight. This happens if the user
    // mouses directly into the results list from outside the popup.
    this.events.subscribe('results-list-mouse-highlighted', this.clearRecommendationHighlight);
  },
  detachMouseListeners: function() {
    this.events.unsubscribe('recommendation-mouseenter', this.stealHighlight);
    this.events.unsubscribe('recommendation-mouseleave', this.onRecommendationMouseLeave);
    this.events.unsubscribe('recommendation-mousemove', this.onRecommendationMouseMove);
    this.events.unsubscribe('results-list-mouse-highlighted', this.clearRecommendationHighlight);
  },
  onRecommendationMouseEnter: function() {
    // See attachMouseListeners() for documentation.
    this.popup.el.richlistbox.selectedIndex = -1;
    this.recommendation.isHighlighted = true;
  },
  onRecommendationMouseMove: function() {
    // See attachMouseListeners() for documentation.
    this.popup.el.richlistbox.selectedIndex = -1;
    this.recommendation.isHighlighted = true;
  },
  onRecommendationMouseLeave: function(evt) {
    // See attachMouseListeners() for documentation.

    // relatedTarget seems to be null when the user mouses out of the popup,
    // even though the pointer may be entering other parts of the XUL DOM.
    if (evt.relatedTarget && evt.relatedTarget.closest('.autocomplete-richlistbox')) {
      this.recommendation.isHighlighted = false;
    }
  },
  initMutationObserver: function() {
    // Use a MutationObserver to detect when results are inserted into the
    // popup. Unfortunately, while the history and suggestions searches return
    // results async, there's no clean way to detect new results in pure JS.
    //
    // Listen for changes to the `url` attribute on the rows inside the results
    // richlistbox. This ensures changes are detected, even when rows are
    // reused, rather than created (see the use of maxRows in _adjustAcItem,
    // inside autocomplete.xml).
    this.resultsObserver = new this.win.MutationObserver(this.mutationHandler);
    const results = this.win.document.getAnonymousElementByAttribute(this.popup.el, 'anonid', 'richlistbox');
    const resultsObserverConfig = {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['url']
    };
    this.resultsObserver.observe(results, resultsObserverConfig);
  },
  mutationHandler: function() {
    // If the recommendation exists and is visible, steal the highlight.
    if (this.recommendation.el && !this.recommendation.el.collapsed) {
      this.stealHighlight();
    }
  },
  stealHighlight: function() {
    // If the recommendation is shown, the default behavior is for the urlbar
    // to have the highlight. That is, neither the recommendation nor any items
    // in the results list should be highlighted.
    //
    // We need to retake control of the highlight under two circumstances:
    // 1. When the recommendation has just been shown, if the user isn't keying
    //    through the list already, then move the highlight from the top results
    //    list item to the urlbar.
    // 2. After (1), when a few more results have been inserted into the DOM,
    //    the Gecko code will take the highlight again; steal it back.
    //
    // The existing XBL code batches insertions in timeouts. By default,
    // the code invokes setTimeout 5 times, inserting 6 rows per turn, up
    // to the default max of 30 rows.
    //
    // Because the event loop already has up to 5 timers waiting, we can't
    // rely on setTimeout to steal the highlight: our request will be queued
    // behind all those other timers, causing a noticeably long flicker of the
    // blue highlight on the top result row.
    //
    // Our workaround relies on using requestAnimationFrame to cut in line,
    // interleaving our highlight stealing between the setTimeouts.
    //
    // Specifically, each time stealHighlight is called (in response to a DOM
    // mutation fired when rows are inserted into the list), we ask the browser
    // to steal the highlight three times: immediately, just before the next
    // frame is painted (the outer rAF), and just before the frame after that
    // (the inner rAF).
    //
    // This is a weird hack, but it works fairly well. Because setTimeout makes
    // no guarantees about precisely when a callback will be executed, and
    // because the row insertion timeouts are called closely together, ours is
    // a pretty good, though nondeterministic, solution.
    if (!this.recommendation.el || this.recommendation.el.collapsed) {
      return;
    }
    this.clearHighlight();
    this.win.requestAnimationFrame(() => {
      this.clearHighlight();
      this.win.requestAnimationFrame(() => {
        this.clearHighlight();
      });
    });
  },
  clearHighlight: function() {
    // If the selectedIndex > 0, the user is keying through the list, so don't
    // do anything to the highlight.
    if (this.popup.el.richlistbox.selectedIndex > 0) {
      return;
    }
    this.popup.el.richlistbox.selectedIndex = -1;
    this.recommendation.isHighlighted = false;
  },
  clearRecommendationHighlight: function() {
    // See attachMouseListeners() for documentation.
    this.recommendation.isHighlighted = false;
  },
  adjustHighlight: function(evt) {
    // Due to constraints in combining XBL and JS, we have to totally take over
    // highlight management for the list of results as well as the recommendation.
    //
    // adjustHighlight resets the DOM highlight state to what we want.
    // It runs in three phases: first, checking the old state; second, clearing
    // the highlight off of all results (putting the DOM into a dirty state);
    // third, modifying the DOM. The idea is to avoid triggering repaints by
    // batching reads and writes, rather than interleaving them.

    // Batch all DOM access here at the start of the function.
    const resultsContainer = this.win.document.getAnonymousElementByAttribute(this.popup.el, 'anonid', 'richlistbox');
    const resultRows = resultsContainer.querySelectorAll('.autocomplete-richlistitem:not([collapsed])');
    const recommendationUrl = this.recommendation.el.querySelector('#universal-search-recommendation-url');
    const listLength = resultRows.length;
    const selectedIndex = this.popup.el.selectedIndex;
    const recommendationVisible = this.recommendation.el && !this.recommendation.el.collapsed;
    const recommendationHighlighted = this.recommendation.el && this.recommendation.isHighlighted;

    // Here's where we will temporarily store the state updates that we'll
    // actually make at the end:
    //
    // newIndex gives the new index that should be highlighted in the results
    // list, or -1 if the list shouldn't be highlighted.
    let newIndex;
    // newUrl is the URL of the new item. It's set in the urlbar as part of
    // the highlight update.
    let newUrl;
    // Also keep track of what we want to do next with the recommendation.
    let shouldHighlightRecommendation;

    // Clear all highlights. The DOM is now dirty and not trustworthy.
    this.clearHighlight();

    // Now, we look at the state of the recommendation and the results list,
    // and decide what should be highlighted next.

    // These utility functions abstract out the state changes, and make the
    // state transition logic more readable.
    function highlightUrlbar() {
      shouldHighlightRecommendation = false;
      newIndex = -1;
      newUrl = null;
    }

    function highlightRecommendation() {
      shouldHighlightRecommendation = true;
      newIndex = -1;
      newUrl = recommendationUrl.getAttribute('data-url');
    }

    function highlightResult(index) {
      shouldHighlightRecommendation = false;
      newIndex = index;
      newUrl = resultRows[newIndex].getAttribute('url');
    }

    function highlightFirstResult() {
      highlightResult(0);
    }

    function highlightLastResult() {
      highlightResult(resultRows.length - 1);
    }

    // If the urlbar has focus,
    // a 'forward' key moves the highlight to the recommendation, if it's
    // visible, or else the first result;
    // a 'backward' key moves the highlight to the bottom of the list.
    if (!recommendationHighlighted && selectedIndex === -1) {
      if (evt.forward) {
        recommendationVisible ? highlightRecommendation() : highlightFirstResult();
      } else {
        highlightLastResult();
      }

    // If the recommendation was highlighted,
    // a 'forward' key moves the highlight to the first result in the list,
    // a 'backward' key moves the highlight to the urlbar.
    } else if (recommendationHighlighted) {
      if (evt.forward) {
        highlightFirstResult();
      } else {
        highlightUrlbar();
      }

    // If the first result in the list was highlighted,
    // a 'forward' key moves the highlight to the 2nd result in the list,
    // a 'backward' key moves the highlight to the recommendation, if it's
    // visible, or else to the urlbar.
    } else if (selectedIndex === 0) {
      if (evt.forward) {
        highlightResult(1);
      } else {
        recommendationVisible ? highlightRecommendation() : highlightUrlbar();
      }

    // If the last result in the list was highlighted,
    // a 'forward' key moves the highlight to the urlbar, scrolling the
    // results list back to the first item,
    // a 'backward' key moves the highlight to the result 2nd from the end
    // of the list.
    //
    // It turns out that scrolling the results list (the 'forward' case)
    // causes the results list to retake the highlight. If the recommendation
    // is shown, we want to steal the highlight back for the urlbar. (If not,
    // the top result item will be highlighted, the expected behavior.)
    //
    // An additional complication: if the mouse pointer is over the popup when
    // the results list scrolls, a mouseover event will be fired on the result
    // that's scrolled underneath the mouse pointer. We want to ignore this
    // event, whether or not the recommendation is visible, so ask the popup
    // to ignore the very next mouseover event it receives.
    } else if (selectedIndex === listLength - 1) {
      if (evt.forward) {
        if (recommendationVisible) {
          this.stealHighlight();
        }
        this.popup.ignoreNextMouseOver = true;
        resultsContainer.ensureIndexIsVisible(0);
        highlightUrlbar();
      } else {
        highlightResult(selectedIndex - 1);
      }

    // If the highlighted result was in the middle of the list,
    // then move the highlight to one of its neighbors.
    } else {
      if (evt.forward) {
        highlightResult(selectedIndex + 1);
      } else {
        highlightResult(selectedIndex - 1);
      }
    }

    // Now that we've figured out which item will take the highlight,
    // update the DOM, and fire a signal that sets the newUrl in the urlbar.
    this.popup.el.selectedIndex = newIndex;
    this.recommendation.isHighlighted = shouldHighlightRecommendation;
    this.events.publish('selection-change', { newUrl: newUrl });
  }
};
<commit_msg>Always steal the highlight when the popup is shown or new results appear<commit_after>/* 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/. */

const EXPORTED_SYMBOLS = ['HighlightManager'];

function HighlightManager(opts) {
  /*
   The Highlight Manager abstracts out the task of adjusting the blue highlight
   in the autocomplete popup in response to key and mouse events, as well as
   in response to the recommendation being shown.
   */
  this.win = opts.win;
  this.events = opts.events;
  this.popup = opts.popup;
  // this is a recommendation view, because the highlight
  // class manages passing focus between it and the list of results
  this.recommendation = opts.recommendation;

  // hasInteracted tracks whether the user has moused over the popup, or keyed
  // through the popup, since the last urlbar input (typed printable keys or
  // pasted text). If the user has not interacted with the popup, it's safe to
  // steal the highlight.
  this.hasInteracted = false;

  this.adjustHighlight = this.adjustHighlight.bind(this);
  this.onResultsListMouseOver = this.onResultsListMouseOver.bind(this);
  this.initMutationObserver = this.initMutationObserver.bind(this);
  this.onRecommendationMouseEnter = this.onRecommendationMouseEnter.bind(this);
  this.onRecommendationMouseLeave = this.onRecommendationMouseLeave.bind(this);
  this.onRecommendationMouseMove = this.onRecommendationMouseMove.bind(this);
  this.resetInteractionState = this.resetInteractionState.bind(this);
  this.onUrlbarChange = this.onUrlbarChange.bind(this);
  this.stealHighlight = this.stealHighlight.bind(this);
}

HighlightManager.prototype = {
  init: function() {
    this.events.subscribe('navigational-key', this.adjustHighlight);
    // It seems that the recommendation is rarely in the XUL DOM when this init
    // function runs, so listen for the recommendation to be found in the DOM,
    // then try a second time to attach the MutationObserver to the element.
    this.events.subscribe('recommendation-created', this.initMutationObserver);
    this.events.subscribe('recommendation-shown', this.stealHighlight);
    this.events.subscribe('urlbar-change', this.onUrlbarChange);
    this.events.subscribe('before-popup-hide', this.resetInteractionState);

    this.attachMouseListeners();
    this.initMutationObserver();
  },
  destroy: function() {
    this.resultsObserver.disconnect();
    this.detachMouseListeners();

    this.events.unsubscribe('recommendation-created', this.initMutationObserver);
    this.events.unsubscribe('recommendation-shown', this.stealHighlight);
    this.events.unsubscribe('navigational-key', this.adjustHighlight);
    this.events.unsubscribe('urlbar-change', this.onUrlbarChange);
    this.events.subscribe('before-popup-hide', this.resetInteractionState);

    delete this.popup;
    delete this.recommendation;
    delete this.win;
  },
  attachMouseListeners: function() {
    // This function is important because it gathers docs for all the mouse
    // event handlers in a single place.

    // When the user mouses into the recommendation, the results list might
    // have had the focus, so swap the highlight. No need to resort to stealing
    // the highlight, because there's no race to update the DOM.
    this.events.subscribe('recommendation-mouseenter', this.onRecommendationMouseEnter);

    // The mouseenter listener fails to apply the highlight to the
    // recommendation in edge cases where the highlight is applied to the
    // results list when the mouse pointer is inside the recommendation. When
    // the user starts moving the mouse, no mouseenter event is fired, because
    // the mouse pointer was already inside the element. This most often
    // happens when the stealHighlight() method fails to steal the highlight
    // (stealHighlight is non-deterministic), but can also reproducibly occur
    // in flows like this example:
    // 1. User mouses over the recommendation, highlighting it
    // 2. User keys away from the recommendation, highlighting a result in the
    //    results list
    // 3. User wiggles the mouse inside the recommendation: no mouseenter is
    //    fired, so no highlight is applied without a mousemove listener.
    //
    // While this is an edge case, the listener is simple, and makes the mouse
    // highlight management code very robust against timing errors.
    this.events.subscribe('recommendation-mousemove', this.onRecommendationMouseMove);

    // When the user mouses out of the recommendation, if the mouse is leaving
    // the popup, keep the recommendation highlighted, matching the mouseout
    // behavior of the results list. If the mouse is moving to the results
    // list, remove the highlight from the recommendation.
    this.events.subscribe('recommendation-mouseleave', this.onRecommendationMouseLeave);

    // When the user mouses over the results list, the recommendation may have
    // the highlight, and should lose the highlight. This happens if the user
    // mouses directly into the results list from outside the popup.
    this.events.subscribe('results-list-mouse-highlighted', this.onResultsListMouseOver);
  },
  detachMouseListeners: function() {
    this.events.unsubscribe('recommendation-mouseenter', this.stealHighlight);
    this.events.unsubscribe('recommendation-mouseleave', this.onRecommendationMouseLeave);
    this.events.unsubscribe('recommendation-mousemove', this.onRecommendationMouseMove);
    this.events.unsubscribe('results-list-mouse-highlighted', this.onResultsListMouseOver);
  },
  onRecommendationMouseEnter: function() {
    // See attachMouseListeners() for documentation.
    this.hasInteracted = true;
    this.popup.el.richlistbox.selectedIndex = -1;
    this.recommendation.isHighlighted = true;
  },
  onRecommendationMouseMove: function() {
    // See attachMouseListeners() for documentation.
    this.hasInteracted = true;
    this.popup.el.richlistbox.selectedIndex = -1;
    this.recommendation.isHighlighted = true;
  },
  onRecommendationMouseLeave: function(evt) {
    // See attachMouseListeners() for documentation.

    // relatedTarget seems to be null when the user mouses out of the popup,
    // even though the pointer may be entering other parts of the XUL DOM.
    if (evt.relatedTarget && evt.relatedTarget.closest('.autocomplete-richlistbox')) {
      this.recommendation.isHighlighted = false;
    }
  },
  onResultsListMouseOver: function() {
    // See attachMouseListeners() for documentation.
    this.hasInteracted = true;
    this.recommendation.isHighlighted = false;
  },
  resetInteractionState: function() {
    // Because clearHighlight checks the `hasInteracted` state, unset it when
    // the popup closes, not when it reopens, to avoid races that could lead
    // to the highlight not being correctly stolen. See also the hasInteracted
    // docs in the constructor.
    this.hasInteracted = false;
  },
  onUrlbarChange: function() {
    // When the contents of the urlbar are updated, we need to reset the
    // interaction state, and re-steal the highlight (new results will be
    // rendered in response to the urlbar change).
    this.resetInteractionState();
    this.stealHighlight();
  },
  initMutationObserver: function() {
    // Use a MutationObserver to detect when results are inserted into the
    // popup. Unfortunately, while the history and suggestions searches return
    // results async, there's no clean way to detect new results in pure JS.
    //
    // Listen for changes to the `url` attribute on the rows inside the results
    // richlistbox. This ensures changes are detected, even when rows are
    // reused, rather than created (see the use of maxRows in _adjustAcItem,
    // inside autocomplete.xml).
    this.resultsObserver = new this.win.MutationObserver(this.stealHighlight);
    const results = this.win.document.getAnonymousElementByAttribute(this.popup.el, 'anonid', 'richlistbox');
    const resultsObserverConfig = {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['url']
    };
    this.resultsObserver.observe(results, resultsObserverConfig);
  },
  stealHighlight: function() {
    // Current default Firefox behavior is to highlight the top 'heuristic'
    // result when the popup is first shown. For a given input string foo, the
    // heuristic result either reads, "Visit foo", if foo looks like a URL,
    // or it reads, "foo - Search with $searchProvider".
    //
    // We want to remove the highlight from the top result, so that it appears
    // that the urlbar is focused when the popup first appears.
    //
    // The Firefox autocomplete popup implementation makes stealing the
    // highlight a bit complicated:
    //
    // The existing XBL code batches insertions in timeouts. By default,
    // the code invokes setTimeout 5 times, inserting 6 rows per turn, up
    // to the default max of 30 rows.
    //
    // Because the event loop already has up to 5 timers waiting, we can't
    // rely on setTimeout to steal the highlight: our request will be queued
    // behind all those other timers, causing a noticeably long flicker of the
    // blue highlight on the top result row.
    //
    // Our workaround relies on using requestAnimationFrame to cut in line,
    // interleaving our highlight stealing between the setTimeouts.
    //
    // Specifically, each time stealHighlight is called (in response to a DOM
    // mutation fired when rows are inserted into the list), we ask the browser
    // to steal the highlight six times: immediately, just before the next
    // frame is painted (the outer rAF), and just before the next four frames
    // (the inner rAF calls).
    //
    // This is a weird hack, but it works fairly well. Because setTimeout makes
    // no guarantees about precisely when a callback will be executed, and
    // because the row insertion timeouts are called closely together, ours is
    // a pretty good, though nondeterministic, solution.
    this.clearHighlight();
    this.win.requestAnimationFrame(() => {
      this.clearHighlight();
      this.win.requestAnimationFrame(() => {
        this.clearHighlight();
        this.win.requestAnimationFrame(() => {
          this.clearHighlight();
          this.win.requestAnimationFrame(() => {
            this.clearHighlight();
            this.win.requestAnimationFrame(() => {
              this.clearHighlight();
            });
          });
        });
      });
    });
  },
  clearHighlight: function() {
    // If the user has interacted with the popup since the last urlbar input,
    // don't mess with the highlight.
    if (this.hasInteracted) {
      return;
    }
    this.popup.el.richlistbox.selectedIndex = -1;
    this.recommendation.isHighlighted = false;
  },
  adjustHighlight: function(evt) {
    // Due to constraints in combining XBL and JS, we have to totally take over
    // highlight management for the list of results as well as the recommendation.
    //
    // adjustHighlight resets the DOM highlight state to what we want.
    // It runs in three phases: first, checking the old state; second, clearing
    // the highlight off of all results (putting the DOM into a dirty state);
    // third, modifying the DOM. The idea is to avoid triggering repaints by
    // batching reads and writes, rather than interleaving them.

    // Batch all DOM access here at the start of the function.
    const resultsContainer = this.win.document.getAnonymousElementByAttribute(this.popup.el, 'anonid', 'richlistbox');
    const resultRows = resultsContainer.querySelectorAll('.autocomplete-richlistitem:not([collapsed])');
    const recommendationUrl = this.recommendation.el.querySelector('#universal-search-recommendation-url');
    const listLength = resultRows.length;
    const selectedIndex = this.popup.el.selectedIndex;
    const recommendationVisible = this.recommendation.el && !this.recommendation.el.collapsed;
    const recommendationHighlighted = this.recommendation.el && this.recommendation.isHighlighted;

    // Here's where we will temporarily store the state updates that we'll
    // actually make at the end:
    //
    // newIndex gives the new index that should be highlighted in the results
    // list, or -1 if the list shouldn't be highlighted.
    let newIndex;
    // newUrl is the URL of the new item. It's set in the urlbar as part of
    // the highlight update.
    let newUrl;
    // Also keep track of what we want to do next with the recommendation.
    let shouldHighlightRecommendation;

    // Clear all highlights. The DOM is now dirty and not trustworthy.
    this.clearHighlight();

    // Now, we look at the state of the recommendation and the results list,
    // and decide what should be highlighted next.

    // These utility functions abstract out the state changes, and make the
    // state transition logic more readable.
    function highlightUrlbar() {
      shouldHighlightRecommendation = false;
      newIndex = -1;
      newUrl = null;
    }

    function highlightRecommendation() {
      shouldHighlightRecommendation = true;
      newIndex = -1;
      newUrl = recommendationUrl.getAttribute('data-url');
    }

    function highlightResult(index) {
      shouldHighlightRecommendation = false;
      newIndex = index;
      newUrl = resultRows[newIndex].getAttribute('url');
    }

    function highlightFirstResult() {
      highlightResult(0);
    }

    function highlightLastResult() {
      highlightResult(resultRows.length - 1);
    }

    // If the urlbar has focus,
    // a 'forward' key moves the highlight to the recommendation, if it's
    // visible, or else the first result;
    // a 'backward' key moves the highlight to the bottom of the list.
    if (!recommendationHighlighted && selectedIndex === -1) {
      if (evt.forward) {
        recommendationVisible ? highlightRecommendation() : highlightFirstResult();
      } else {
        highlightLastResult();
      }

    // If the recommendation was highlighted,
    // a 'forward' key moves the highlight to the first result in the list,
    // a 'backward' key moves the highlight to the urlbar.
    } else if (recommendationHighlighted) {
      if (evt.forward) {
        highlightFirstResult();
      } else {
        highlightUrlbar();
      }

    // If the first result in the list was highlighted,
    // a 'forward' key moves the highlight to the 2nd result in the list,
    // a 'backward' key moves the highlight to the recommendation, if it's
    // visible, or else to the urlbar.
    } else if (selectedIndex === 0) {
      if (evt.forward) {
        highlightResult(1);
      } else {
        recommendationVisible ? highlightRecommendation() : highlightUrlbar();
      }

    // If the last result in the list was highlighted,
    // a 'forward' key moves the highlight to the urlbar, scrolling the
    // results list back to the first item,
    // a 'backward' key moves the highlight to the result 2nd from the end
    // of the list.
    //
    // It turns out that scrolling the results list (the 'forward' case)
    // causes the top result in the results list to retake the highlight, so we
    // steal it back.
    //
    // An additional complication: if the mouse pointer is over the popup when
    // the results list scrolls, a mouseover event will be fired on the result
    // that's scrolled underneath the mouse pointer. We want to ignore this
    // event, whether or not the recommendation is visible, so ask the popup
    // to ignore the very next mouseover event it receives.
    } else if (selectedIndex === listLength - 1) {
      if (evt.forward) {
        this.stealHighlight();
        this.popup.ignoreNextMouseOver = true;
        resultsContainer.ensureIndexIsVisible(0);
        highlightUrlbar();
      } else {
        highlightResult(selectedIndex - 1);
      }

    // If the highlighted result was in the middle of the list,
    // then move the highlight to one of its neighbors.
    } else {
      if (evt.forward) {
        highlightResult(selectedIndex + 1);
      } else {
        highlightResult(selectedIndex - 1);
      }
    }

    // Now that we've figured out which item will take the highlight,
    // update the DOM, and fire a signal that sets the newUrl in the urlbar.
    this.popup.el.selectedIndex = newIndex;
    this.recommendation.isHighlighted = shouldHighlightRecommendation;
    this.events.publish('selection-change', { newUrl: newUrl });
  }
};
<|endoftext|>


<commit_before>var log = require('../../logger.js');
var MAX_CALLS = [10, 1, 0];
var METHODS = ['setTimeout', 'setInterval', 'requestAnimationFrame'];

function validate(harvested, report, next) {

    if (!(harvested.timers)) {
        log.error('Missing harvested.timers', harvested.timers);
        return next();
    }

    METHODS.forEach(function (method, methodIndex) {
        // filter out internal

        var list = harvested.timers[method];
        if (!list || list.length === 0) {
            return;
        }

        // handle lists in lists
        var listsInLists = Array.isArray(list[0]);

        if (listsInLists) {
            list.forEach(collectionInCollection);
        } else {
            collectionInCollection(list, 0, list);
        }

        function collectionInCollection(collection, index, list) {
            collection = collection.filter(isHttp);
            var msg;
            var trace = {trace: collection.map(function(v){
                return v.trace;
            })};
            if (collection.length > MAX_CALLS[methodIndex]) {
                msg = ['Overusage of ', method, '.', ' In practice ', collection.length, ' times when maximum is ', (MAX_CALLS[methodIndex])].join('');
                report.error(msg, trace);
            } else if (collection.length > 0 && collection.length <= MAX_CALLS[methodIndex]) {
                msg = ['Usage of ', method, '.', ' ', collection.length, ' times used when maximum is ', (MAX_CALLS[methodIndex])].join('');
                report.warn(msg, trace);
            }
        }

    });
    next();
}

function isHttp(entry) {
    return (entry && entry.trace && entry.trace.sourceURL && entry.trace.sourceURL.indexOf('http') === 0);
}

module.exports = {
    dependencies: ['timers'],
    validate: validate
};
<commit_msg>Allow 20 setTimeouts, and only warn if close to usage limit<commit_after>var log = require('../../logger.js');
var MAX_CALLS = [20, 1, 0];
var METHODS = ['setTimeout', 'setInterval', 'requestAnimationFrame'];

function validate(harvested, report, next) {

    if (!(harvested.timers)) {
        log.error('Missing harvested.timers', harvested.timers);
        return next();
    }

    METHODS.forEach(function (method, methodIndex) {
        // filter out internal
        var max = MAX_CALLS[methodIndex];
        var list = harvested.timers[method];
        if (!list || list.length === 0) {
            return;
        }

        // handle lists in lists
        var listsInLists = Array.isArray(list[0]);

        if (listsInLists) {
            list.forEach(collectionInCollection);
        } else {
            collectionInCollection(list, 0, list);
        }

        function collectionInCollection(collection, index, list) {
            collection = collection.filter(isHttp);
            var msg;
            var trace = {trace: collection.map(function(v){
                return v.trace;
            })};

            if (collection.length > max) {
                msg = ['Overusage of ', method, '.', ' In practice ', collection.length, ' times when maximum is ', (max)].join('');
                report.error(msg, trace);
            } else if (collection.length > (max > 1 ? max / 2 : 0) && collection.length <= max) {
                msg = ['Usage of ', method, '.', ' ', collection.length, ' times used when maximum is ', (max)].join('');
                report.warn(msg, trace);
            }
        }

    });
    next();
}

function isHttp(entry) {
    return (entry && entry.trace && entry.trace.sourceURL && entry.trace.sourceURL.indexOf('http') === 0);
}

module.exports = {
    dependencies: ['timers'],
    validate: validate
};
<|endoftext|>


<commit_before>'use strict';

var path = require('path')
var spawn = require('child_process').spawn

var through = require('through2')

var npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
var nodemon = process.platform === 'win32' ? 'nodemon.cmd' : 'nodemon';

module.exports = function watchPackage(pkgDir, exit) {
  var pkg = require(path.join(pkgDir, 'package.json'))
  var processes = {}

  if (typeof pkg.watch !== 'object') {
    die('No "watch" config in package.json')
  }

  // send 'rs' commands to the right proc
  var stdin = through(function (line, _, callback) {
    line = line.toString()
    var match = line.match(/^rs\s+(\w+)/)
    if (!match) {
      console.log('Unrecognized input:', line)
      return callback()
    }
    var proc = processes[match[1]]
    if (!proc) {
      console.log('Couldn\'t find process:', match[1])
      return callback()
    }
    proc.stdin.write('rs\n')
    callback()
  })

  stdin.stderr = through()
  stdin.stdout = through()

  Object.keys(pkg.watch).forEach(function (script) {
    if (!pkg.scripts[script]) {
      die('No such script "' + script + '"', 2)
    }
    var exec = [npm, 'run', script].join(' ')
    var patterns = null
    var extensions = null
    var ignores = null
    var quiet = null
    var inherit = null

    if (typeof pkg.watch[script] === 'object' && !Array.isArray(pkg.watch[script])) {
      patterns = pkg.watch[script].patterns
      extensions = pkg.watch[script].extensions
      ignores = pkg.watch[script].ignore
      quiet = pkg.watch[script].quiet
      inherit = pkg.watch[script].inherit
    } else {
      patterns = pkg.watch[script]
    }

    patterns = [].concat(patterns).map(function (pattern) {
      return ['--watch', pattern]
    }).reduce(function (a, b) {
      return a.concat(b)
    })

    if (ignores) {
      ignores = [].concat(ignores).map(function (ignore) {
        return ['--ignore', ignore]
      }).reduce(function (a, b) {
        return a.concat(b)
      })
    }

    var args = extensions ? ['--ext', extensions] : []
    args = args.concat(patterns)
    if (ignores) { args = args.concat(ignores) }
    args = args.concat(['--exec', exec])
    var proc = processes[script] = spawn(nodemon, args, {
      env: process.env,
      cwd: pkgDir,
      stdio: inherit === true ? ['pipe', 'inherit', 'pipe'] : 'pipe'
    })
    if (inherit === true) return;
    if (quiet === true || quiet === 'true') {
      proc.stdout.pipe(stdin.stdout)
      proc.stderr.pipe(stdin.stderr)
    } else {
      proc.stdout.pipe(prefixer('[' + script + ']')).pipe(stdin.stdout)
      proc.stderr.pipe(prefixer('[' + script + ']')).pipe(stdin.stderr)
    }
  })

  return stdin

  function die(message, code) {
    process.stderr.write(message)

    if (stdin) {
      stdin.end()
      stdin.stderr.end()
      stdin.stdout.end()
    }
    exit(code || 1)
  }
}

function prefixer(prefix) {
  return through(function (line, _, callback) {
    line = line.toString()
    if (!line.match('to restart at any time')) {
      this.push(prefix + ' ' + line)
    }
    callback()
  })
}
<commit_msg>Silent NPM for readability<commit_after>'use strict';

var path = require('path')
var spawn = require('child_process').spawn

var through = require('through2')

var npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
var nodemon = process.platform === 'win32' ? 'nodemon.cmd' : 'nodemon';

module.exports = function watchPackage(pkgDir, exit) {
  var pkg = require(path.join(pkgDir, 'package.json'))
  var processes = {}

  if (typeof pkg.watch !== 'object') {
    die('No "watch" config in package.json')
  }

  // send 'rs' commands to the right proc
  var stdin = through(function (line, _, callback) {
    line = line.toString()
    var match = line.match(/^rs\s+(\w+)/)
    if (!match) {
      console.log('Unrecognized input:', line)
      return callback()
    }
    var proc = processes[match[1]]
    if (!proc) {
      console.log('Couldn\'t find process:', match[1])
      return callback()
    }
    proc.stdin.write('rs\n')
    callback()
  })

  stdin.stderr = through()
  stdin.stdout = through()

  Object.keys(pkg.watch).forEach(function (script) {
    if (!pkg.scripts[script]) {
      die('No such script "' + script + '"', 2)
    }
    var exec = [npm, 'run', '-s', script].join(' ')
    var patterns = null
    var extensions = null
    var ignores = null
    var quiet = null
    var inherit = null

    if (typeof pkg.watch[script] === 'object' && !Array.isArray(pkg.watch[script])) {
      patterns = pkg.watch[script].patterns
      extensions = pkg.watch[script].extensions
      ignores = pkg.watch[script].ignore
      quiet = pkg.watch[script].quiet
      inherit = pkg.watch[script].inherit
    } else {
      patterns = pkg.watch[script]
    }

    patterns = [].concat(patterns).map(function (pattern) {
      return ['--watch', pattern]
    }).reduce(function (a, b) {
      return a.concat(b)
    })

    if (ignores) {
      ignores = [].concat(ignores).map(function (ignore) {
        return ['--ignore', ignore]
      }).reduce(function (a, b) {
        return a.concat(b)
      })
    }

    var args = extensions ? ['--ext', extensions] : []
    args = args.concat(patterns)
    if (ignores) { args = args.concat(ignores) }
    args = args.concat(['--exec', exec])
    var proc = processes[script] = spawn(nodemon, args, {
      env: process.env,
      cwd: pkgDir,
      stdio: inherit === true ? ['pipe', 'inherit', 'pipe'] : 'pipe'
    })
    if (inherit === true) return;
    if (quiet === true || quiet === 'true') {
      proc.stdout.pipe(stdin.stdout)
      proc.stderr.pipe(stdin.stderr)
    } else {
      proc.stdout.pipe(prefixer('[' + script + ']')).pipe(stdin.stdout)
      proc.stderr.pipe(prefixer('[' + script + ']')).pipe(stdin.stderr)
    }
  })

  return stdin

  function die(message, code) {
    process.stderr.write(message)

    if (stdin) {
      stdin.end()
      stdin.stderr.end()
      stdin.stdout.end()
    }
    exit(code || 1)
  }
}

function prefixer(prefix) {
  return through(function (line, _, callback) {
    line = line.toString()
    if (!line.match('to restart at any time')) {
      this.push(prefix + ' ' + line)
    }
    callback()
  })
}
<|endoftext|>


<commit_before>'use strict';

/* global chai: false */

var expect = chai.expect;

describe('easyRTCService service', function() {
  var service, tokenAPI, session, webrtcFactory, easyrtc, currentConferenceState, disconnectCallback;

  beforeEach(angular.mock.module('op.live-conference'));

  beforeEach(function() {
    currentConferenceState = {};
    easyrtc = {
      setGotMedia: function() {},
      setDataChannelCloseListener: function() {},
      setCallCancelled: function() {},
      setOnStreamClosed: function() {},
      getVideoSourceList: function() {},
      enableDataChannels: function() {},
      setDisconnectListener: function(callback) { disconnectCallback = callback; },
      myEasyrtcid: 'myself'
    };
    tokenAPI = {};
    session = {
      getUsername: function() { return 'Wooot'; },
      getUserId: function() { return 2; },
      user: {
        _id: 2,
        emails: ['test@openpaas.io']
      },
      domain: {
        _id: 123
      }
    };
    webrtcFactory = {
      get: function() {
        return easyrtc;
      }
    };

    module(function($provide) {
      $provide.value('tokenAPI', {});
      $provide.value('session', session);
      $provide.value('webrtcFactory', webrtcFactory);
      $provide.value('ioSocketConnection', {});
      $provide.value('ioConnectionManager', {});
      $provide.value('currentConferenceState', currentConferenceState);
    });

    inject(function($injector) {
      service = $injector.get('easyRTCService');
    });
  });

  describe('broadcastData function', function() {

    it('should do nothing if easyrtc.getRoomOccupantsAsArray fails to return the occupants', function() {
      easyrtc.getRoomOccupantsAsArray = function() { return null; };
      easyrtc.sendData = function() { throw new Error('This test should not call easyrtc.sendData'); };

      service.broadcastData('', {});
    });

    it('should call easyrtc.sendData on each occupant except myself', function() {
      var calledIds = [];

      easyrtc.getRoomOccupantsAsArray = function() { return ['myself', 'other1', 'other2']; };
      easyrtc.sendData = function(easyrtcid, event, data) {
        expect(event).to.equal('message');
        expect(data).to.deep.equal({ da: 'ta' });

        calledIds.push(easyrtcid);
      };

      service.broadcastData('message', { da: 'ta' });

      expect(calledIds).to.deep.equal(['other1', 'other2']);
    });

  });

  describe('broadcastMe function', function() {

    it('should call broadcastData with a prepared attendee', function() {
      currentConferenceState.getAttendeeByEasyrtcid = function() {
        return {
          index: 0,
          videoId: 'videoId',
          id: 'id',
          easyrtcid: 'easyrtcid',
          displayName: 'displayName',
          avatar: 'avatar',
          mute: true,
          muteVideo: false,
          speaking: false,
          foo: 'bar'
        };
      };
      easyrtc.getRoomOccupantsAsArray = function() { return ['myself', 'other1', 'other2']; };
      easyrtc.sendData = function(easyrtcid, event, data) {
        expect(data).to.deep.equal({
          id: 'id',
          easyrtcid: 'easyrtcid',
          displayName: 'displayName',
          avatar: 'avatar',
          mute: true,
          muteVideo: false,
          speaking: false
        });
      };

      service.broadcastMe();
    });

    it('should do nothing if attendee cannot be found', function() {
      currentConferenceState.getAttendeeByEasyrtcid = function() { return null; };
      easyrtc.getRoomOccupantsAsArray = function() {
        throw new Error('This test should not call easyrtc.getRoomOccupantsAsArray.');
      };

      service.broadcastMe();
    });

  });

  describe('addDisconnectCallback function', function() {

    it('should return false if no callback is given', function() {
      expect(service.addDisconnectCallback()).to.be.false;
    });

    it('should register a new disconnect callback', function(done) {
      service.addDisconnectCallback(done);

      disconnectCallback();
    });

    it('should return an identifier for the registered callback', function() {
      expect(service.addDisconnectCallback(function() {})).to.exist;
    });

  });

  describe('removeDisconnectCallback function', function() {

    it('should return false if no id is given', function() {
      expect(service.removeDisconnectCallback()).to.be.false;
    });

    it('should remove an existing disconnect callback', function() {
      var id = service.addDisconnectCallback(function() {
        throw new Error('This test should not call any disconnect callback !');
      });

      service.removeDisconnectCallback(id);
      disconnectCallback();
    });

  });

  describe('setGotMedia function', function() {

    it('should proxy to easyrtc.setGotMedia()', function(done) {

      var callback = function() {
      };

      easyrtc.setGotMedia = function(arg) {
        expect(arg).to.equal(callback);
        done();
      };
      service.setGotMedia(callback);
    });
  });

  describe('sendData function', function() {

    it('should forward the call to easyrtc.sendData', function(done) {
      var testId = 'anId';
      var testMsgType = 'aType';
      var testData = {
        toto: 'titi',
        tata: {}
      };
      var testHandler = function() {};

      easyrtc.sendData = function(easyrtcid, msgType, data, ackhandler) {
        expect(easyrtcid).to.equal(testId);
        expect(msgType).to.equal(testMsgType);
        expect(data).to.equal(JSON.stringify(testData));
        expect(ackhandler).to.deep.equal(testHandler);
        done();
      };

      service.sendData(testId, testMsgType, testData, testHandler);
    });
  });

});
<commit_msg>MEET-338 tests for listenerFactory<commit_after>'use strict';

/* global chai: false */

var expect = chai.expect;

describe('easyRTCService service', function() {
  var service, tokenAPI, session, webrtcFactory, easyrtc, currentConferenceState, disconnectCallback;

  beforeEach(angular.mock.module('op.live-conference'));

  beforeEach(function() {
    currentConferenceState = {};
    easyrtc = {
      setGotMedia: function() {},
      setDataChannelCloseListener: function() {},
      setCallCancelled: function() {},
      setOnStreamClosed: function() {},
      getVideoSourceList: function() {},
      enableDataChannels: function() {},
      setDisconnectListener: function(callback) { disconnectCallback = callback; },
      myEasyrtcid: 'myself'
    };
    tokenAPI = {};
    session = {
      getUsername: function() { return 'Wooot'; },
      getUserId: function() { return 2; },
      user: {
        _id: 2,
        emails: ['test@openpaas.io']
      },
      domain: {
        _id: 123
      }
    };
    webrtcFactory = {
      get: function() {
        return easyrtc;
      }
    };

    module(function($provide) {
      $provide.value('tokenAPI', {});
      $provide.value('session', session);
      $provide.value('webrtcFactory', webrtcFactory);
      $provide.value('ioSocketConnection', {});
      $provide.value('ioConnectionManager', {});
      $provide.value('currentConferenceState', currentConferenceState);
    });

    inject(function($injector) {
      service = $injector.get('easyRTCService');
    });
  });

  describe('broadcastData function', function() {

    it('should do nothing if easyrtc.getRoomOccupantsAsArray fails to return the occupants', function() {
      easyrtc.getRoomOccupantsAsArray = function() { return null; };
      easyrtc.sendData = function() { throw new Error('This test should not call easyrtc.sendData'); };

      service.broadcastData('', {});
    });

    it('should call easyrtc.sendData on each occupant except myself', function() {
      var calledIds = [];

      easyrtc.getRoomOccupantsAsArray = function() { return ['myself', 'other1', 'other2']; };
      easyrtc.sendData = function(easyrtcid, event, data) {
        expect(event).to.equal('message');
        expect(data).to.deep.equal({ da: 'ta' });

        calledIds.push(easyrtcid);
      };

      service.broadcastData('message', { da: 'ta' });

      expect(calledIds).to.deep.equal(['other1', 'other2']);
    });

  });

  describe('broadcastMe function', function() {

    it('should call broadcastData with a prepared attendee', function() {
      currentConferenceState.getAttendeeByEasyrtcid = function() {
        return {
          index: 0,
          videoId: 'videoId',
          id: 'id',
          easyrtcid: 'easyrtcid',
          displayName: 'displayName',
          avatar: 'avatar',
          mute: true,
          muteVideo: false,
          speaking: false,
          foo: 'bar'
        };
      };
      easyrtc.getRoomOccupantsAsArray = function() { return ['myself', 'other1', 'other2']; };
      easyrtc.sendData = function(easyrtcid, event, data) {
        expect(data).to.deep.equal({
          id: 'id',
          easyrtcid: 'easyrtcid',
          displayName: 'displayName',
          avatar: 'avatar',
          mute: true,
          muteVideo: false,
          speaking: false
        });
      };

      service.broadcastMe();
    });

    it('should do nothing if attendee cannot be found', function() {
      currentConferenceState.getAttendeeByEasyrtcid = function() { return null; };
      easyrtc.getRoomOccupantsAsArray = function() {
        throw new Error('This test should not call easyrtc.getRoomOccupantsAsArray.');
      };

      service.broadcastMe();
    });

  });

  describe('addDisconnectCallback function', function() {

    it('should return false if no callback is given', function() {
      expect(service.addDisconnectCallback()).to.be.false;
    });

    it('should register a new disconnect callback', function(done) {
      service.addDisconnectCallback(done);

      disconnectCallback();
    });

    it('should return an identifier for the registered callback', function() {
      expect(service.addDisconnectCallback(function() {})).to.exist;
    });

  });

  describe('removeDisconnectCallback function', function() {

    it('should return false if no id is given', function() {
      expect(service.removeDisconnectCallback()).to.be.false;
    });

    it('should remove an existing disconnect callback', function() {
      var id = service.addDisconnectCallback(function() {
        throw new Error('This test should not call any disconnect callback !');
      });

      service.removeDisconnectCallback(id);
      disconnectCallback();
    });

  });

  describe('setGotMedia function', function() {

    it('should proxy to easyrtc.setGotMedia()', function(done) {

      var callback = function() {
      };

      easyrtc.setGotMedia = function(arg) {
        expect(arg).to.equal(callback);
        done();
      };
      service.setGotMedia(callback);
    });
  });

  describe('sendData function', function() {

    it('should forward the call to easyrtc.sendData', function(done) {
      var testId = 'anId';
      var testMsgType = 'aType';
      var testData = {
        toto: 'titi',
        tata: {}
      };
      var testHandler = function() {};

      easyrtc.sendData = function(easyrtcid, msgType, data, ackhandler) {
        expect(easyrtcid).to.equal(testId);
        expect(msgType).to.equal(testMsgType);
        expect(data).to.equal(JSON.stringify(testData));
        expect(ackhandler).to.deep.equal(testHandler);
        done();
      };

      service.sendData(testId, testMsgType, testData, testHandler);
    });
  });

});

describe('listenerFactory factory', function() {
  var service, DummyCallbackConstructor, dummyCallback, CountCall, listen, emptyFunction;

  DummyCallbackConstructor = function() {
    var callback;
    return {
      setCallback: function(cb) {
        callback = cb;
      },
      callCallback: function() {
        console.log(callback);
        callback();
      }
    };
  };
  CountCall = function() {
    var count = 0;
    return {
      call: function() {
        count++;
      },
      called: function() {
        return count;
      }
    };
  };

  beforeEach(angular.mock.module('op.live-conference'));

  beforeEach(function() {
    inject(function($injector) {
      service = $injector.get('listenerFactory');
    });

    dummyCallback = new DummyCallbackConstructor();
    listen = service(dummyCallback.setCallback);
    emptyFunction = function() { };
  });

  it('should return an object', function(done) {
    expect(listen.addListener).to.be.a('function');
    expect(listen.removeListener).to.be.a('function');
    done();
  });

  describe('addListener function', function() {

    it('should return the last added function', function(done) {
      expect(listen.addListener(emptyFunction)).to.equal(emptyFunction);
      done();
    });

  });

  it('should call each callback once', function(done) {

    var callOnce = new CountCall(), callTwice = new CountCall();

    listen.addListener(callOnce.call);
    listen.addListener(callTwice.call);
    listen.addListener(callTwice.call);

    dummyCallback.callCallback();

    expect(callOnce.called()).to.equal(1);
    expect(callTwice.called()).to.equal(2);
    done();
  });

  it('should be able to remove callbacks', function(done) {
    var callOnce = new CountCall(),
      noCall = new CountCall();

    listen.addListener(noCall.call);
    listen.addListener(noCall.call);
    listen.addListener(noCall.call);

    listen.addListener(callOnce.call);
    listen.removeListener(noCall.call);

    dummyCallback.callCallback();

    expect(callOnce.called()).to.equal(1);
    expect(noCall.called()).to.equal(0);
    done();
  });
});
<|endoftext|>


<commit_before>import Module from 'Parser/Core/Module';
import SPELLS from 'common/SPELLS';

export const DARK_TITAN_ADVICE_ITEM_ID = 137078;
const DARK_TITAN_FINAL_HEALING_INCREASE = 3;

class DarkTitanAdvice extends Module {
  healing = 0;
  lastRealBloomTimestamp = null;
  healingFromProccs = 0;
  on_byPlayer_heal(event) {
    const spellId = event.ability.guid;

    if (spellId === SPELLS.LIFEBLOOM_BLOOM_HEAL.id) {
      // Add 100% of the bloom if it came from the random 3% procc
      // Let's give it 32 ms tolerence for kicks.
      if (this.lastRealBloomTimestamp !== null && (event.timestamp - this.lastRealBloomTimestamp) < 32) {
        this.healing += event.amount;
        this.healingFromProccs += event.amount;
      } else {
        const baseHeal = (event.amount + event.overheal || 0) / DARK_TITAN_FINAL_HEALING_INCREASE;
        this.healing += Math.max(0, event.amount - baseHeal);
      }

      return;
    }
  }

  on_byPlayer_refreshbuff(event) {
    const spellId = event.ability.guid;

    if (spellId === SPELLS.LIFEBLOOM_HOT_HEAL.id) {
      this.lastRealBloomTimestamp = event.timestamp;
    }
  }

  on_byPlayer_removebuff(event) {
    const spellId = event.ability.guid;

    if (spellId === SPELLS.LIFEBLOOM_HOT_HEAL.id) {
      this.lastRealBloomTimestamp = event.timestamp;
    }
  }
}

export default DarkTitanAdvice;
<commit_msg>[resto druid] Fixed healing increase bug in DarkTitanAdvice<commit_after>import Module from 'Parser/Core/Module';
import SPELLS from 'common/SPELLS';

import calculateEffectiveHealing from 'Parser/Core/calculateEffectiveHealing';

export const DARK_TITAN_ADVICE_ITEM_ID = 137078;
const DARK_TITAN_FINAL_HEALING_INCREASE = 2;

class DarkTitanAdvice extends Module {
  healing = 0;
  lastRealBloomTimestamp = null;
  healingFromProccs = 0;
  on_byPlayer_heal(event) {
    const spellId = event.ability.guid;

    if (spellId === SPELLS.LIFEBLOOM_BLOOM_HEAL.id) {
      // Add 100% of the bloom if it came from the random 3% procc
      // Let's give it 32 ms tolerence for kicks.
      if (this.lastRealBloomTimestamp !== null && (event.timestamp - this.lastRealBloomTimestamp) < 32) {
        this.healing += event.amount;
        this.healingFromProccs += event.amount;
      } else {
        this.healing += calculateEffectiveHealing(event, DARK_TITAN_FINAL_HEALING_INCREASE);
      }

      return;
    }
  }

  on_byPlayer_refreshbuff(event) {
    const spellId = event.ability.guid;

    if (spellId === SPELLS.LIFEBLOOM_HOT_HEAL.id) {
      this.lastRealBloomTimestamp = event.timestamp;
    }
  }

  on_byPlayer_removebuff(event) {
    const spellId = event.ability.guid;

    if (spellId === SPELLS.LIFEBLOOM_HOT_HEAL.id) {
      this.lastRealBloomTimestamp = event.timestamp;
    }
  }
}

export default DarkTitanAdvice;
<|endoftext|>


<commit_before>var yauzl = require("../");
var fs = require("fs");
var path = require("path");
var Pend = require("pend");

// git doesn't store empty directories, but we need one for the empty zip file test
(function() {
  var emptyDirectoryPath = path.join(__dirname, "empty");
  if (!fs.existsSync(emptyDirectoryPath)) {
    fs.mkdirSync(emptyDirectoryPath);
  }
})();

var zipfilePaths = fs.readdirSync(__dirname).filter(function(filepath) {
  return /\.zip$/.exec(filepath);
}).map(function(name) {
  return path.relative(".", path.join(__dirname, name));
});
zipfilePaths.sort();

var pend = new Pend();
// 1 thing at a time for reproducibility
pend.max = 1;
zipfilePaths.forEach(function(zipfilePath) {
  var expectedPathPrefix = zipfilePath.replace(/\.zip$/, "");
  // TODO: directories, yo.
  var expectedArchiveContents = {};
  var DIRECTORY = 1; // not a string
  recursiveRead(".");
  function recursiveRead(name) {
    var realPath = path.join(expectedPathPrefix, name);
    if (fs.statSync(realPath).isFile()) {
      expectedArchiveContents[name] = fs.readFileSync(realPath);
    } else {
      if (name !== ".") expectedArchiveContents[name] = DIRECTORY;
      fs.readdirSync(realPath).forEach(function(child) {
        recursiveRead(path.join(name, child));
      });
    }
  }
  pend.go(function(zipfileCallback) {
    yauzl.open(zipfilePath, function(err, zipfile) {
      if (err) throw err;
      var entryProcessing = new Pend();
      entryProcessing.max = 1;
      zipfile.on("entry", function(entry) {
        var messagePrefix = zipfilePath + ": " + entry.fileName + ": ";
        entryProcessing.go(function(entryCallback) {
          var fileNameKey = entry.fileName.replace(/\/$/, "");
          var expectedContents = expectedArchiveContents[fileNameKey];
          if (expectedContents == null) {
            throw new Error(messagePrefix + "not supposed to exist");
          }
          delete expectedArchiveContents[fileNameKey];
          if (entry.fileName !== fileNameKey) {
            // directory
            console.log(messagePrefix + "pass");
            entryCallback();
          } else {
            zipfile.openReadStream(entry, function(err, readStream) {
              if (err) throw err;
              var buffers = [];
              readStream.on("data", function(data) {
                buffers.push(data);
              });
              readStream.on("end", function() {
                var actualContents = Buffer.concat(buffers);
                // uh. there's no buffer equality check?
                var equal = actualContents.toString("binary") === expectedContents.toString("binary");
                if (!equal) {
                  throw new Error(messagePrefix + "wrong contents");
                }
                console.log(messagePrefix + "pass");
                entryCallback();
              });
              readStream.on("error", function(err) {
                throw err;
              });
            });
          }
        });
      });
      zipfile.on("end", function() {
        entryProcessing.wait(function() {
          for (var fileName in expectedArchiveContents) {
            throw new Error(zipfilePath + ": " + fileName + ": missing file");
          }
          console.log(zipfilePath + ": pass");
          zipfileCallback();
        });
      });
    });
  });
});
<commit_msg>test prints 'done' when done, which tests the setImmediate in index.js<commit_after>var yauzl = require("../");
var fs = require("fs");
var path = require("path");
var Pend = require("pend");

// git doesn't store empty directories, but we need one for the empty zip file test
(function() {
  var emptyDirectoryPath = path.join(__dirname, "empty");
  if (!fs.existsSync(emptyDirectoryPath)) {
    fs.mkdirSync(emptyDirectoryPath);
  }
})();

var zipfilePaths = fs.readdirSync(__dirname).filter(function(filepath) {
  return /\.zip$/.exec(filepath);
}).map(function(name) {
  return path.relative(".", path.join(__dirname, name));
});
zipfilePaths.sort();

var pend = new Pend();
// 1 thing at a time for reproducibility
pend.max = 1;
zipfilePaths.forEach(function(zipfilePath) {
  var expectedPathPrefix = zipfilePath.replace(/\.zip$/, "");
  // TODO: directories, yo.
  var expectedArchiveContents = {};
  var DIRECTORY = 1; // not a string
  recursiveRead(".");
  function recursiveRead(name) {
    var realPath = path.join(expectedPathPrefix, name);
    if (fs.statSync(realPath).isFile()) {
      expectedArchiveContents[name] = fs.readFileSync(realPath);
    } else {
      if (name !== ".") expectedArchiveContents[name] = DIRECTORY;
      fs.readdirSync(realPath).forEach(function(child) {
        recursiveRead(path.join(name, child));
      });
    }
  }
  pend.go(function(zipfileCallback) {
    yauzl.open(zipfilePath, function(err, zipfile) {
      if (err) throw err;
      var entryProcessing = new Pend();
      entryProcessing.max = 1;
      zipfile.on("entry", function(entry) {
        var messagePrefix = zipfilePath + ": " + entry.fileName + ": ";
        entryProcessing.go(function(entryCallback) {
          var fileNameKey = entry.fileName.replace(/\/$/, "");
          var expectedContents = expectedArchiveContents[fileNameKey];
          if (expectedContents == null) {
            throw new Error(messagePrefix + "not supposed to exist");
          }
          delete expectedArchiveContents[fileNameKey];
          if (entry.fileName !== fileNameKey) {
            // directory
            console.log(messagePrefix + "pass");
            entryCallback();
          } else {
            zipfile.openReadStream(entry, function(err, readStream) {
              if (err) throw err;
              var buffers = [];
              readStream.on("data", function(data) {
                buffers.push(data);
              });
              readStream.on("end", function() {
                var actualContents = Buffer.concat(buffers);
                // uh. there's no buffer equality check?
                var equal = actualContents.toString("binary") === expectedContents.toString("binary");
                if (!equal) {
                  throw new Error(messagePrefix + "wrong contents");
                }
                console.log(messagePrefix + "pass");
                entryCallback();
              });
              readStream.on("error", function(err) {
                throw err;
              });
            });
          }
        });
      });
      zipfile.on("end", function() {
        entryProcessing.wait(function() {
          for (var fileName in expectedArchiveContents) {
            throw new Error(zipfilePath + ": " + fileName + ": missing file");
          }
          console.log(zipfilePath + ": pass");
          zipfileCallback();
        });
      });
    });
  });
});
pend.wait(function() {
  // if you don't see this, something never happened.
  console.log("done");
});
<|endoftext|>


<commit_before>/*
 * The Web project Gruntfile.
 *
 * Copyright 2013 Palantir Technologies, Inc. All rights reserved.
 */

module.exports = function(grunt) {
  "use strict";

  var path = require("path");
  var cwd = process.cwd();

  var tsJSON = {
    dev: {
      src: ["src/**/*.ts", "typings/**/*.d.ts"],
      out: "build/plottable.js",
      // watch: "src",
      options: {
        target: 'es5',
        noImplicitAny: true,
        sourceMap: false,
        declaration: true,
        removeComments: false
      }
    },
    test: {
      src: ["test/*.ts", "typings/**/*.d.ts", "build/plottable.d.ts"],
      out: "build/tests.js",
      // watch: "test",
      options: {
        target: 'es5',
        sourceMap: false,
        noImplicitAny: true,
        declaration: false,
        removeComments: false
      }
    },
    examples: {
      src: ["examples/*.ts", "typings/**/*.d.ts"],
      outDir: "build",
      // watch: "examples",
      options: {
        target: 'es5',
        sourceMap: false,
        declaration: false,
        removeComments: false
      }
    }
  };

  var bumpJSON = {
    options: {
      files: ['package.json', 'bower.json'],
      updateConfigs: ['pkg'],
      commit: false,
      createTag: false,
      push: false
    }
  }

  var prefixMatch = /\n */;
  var varNameMatch = /[^(:;]*(\([^)]*\))?/; // catch function args too
  var nestedBraceMatch = ": \{[^{}]*\}";
  var typeNameMatch = /: [^;]*/;
  var finalMatch = new RegExp("((" + nestedBraceMatch.source + ")|(" + typeNameMatch.source + "))?;")
  var jsdoc_init = / *\/\*\* *\n/;
  var jsdoc_mid = /( *\*[^\n]*\n)+/;
  var jsdoc_end = / *\*\/ *\n/;
  var jsdoc = new RegExp("(" + jsdoc_init.source + jsdoc_mid.source + jsdoc_end.source + ")?");

  var sedJSON = {
    private_definitions: {
      pattern: new RegExp(jsdoc.source + prefixMatch.source + "private " + varNameMatch.source + finalMatch.source),
      replacement: "",
      path: "build/plottable.d.ts",
    },
    protected_definitions: {
      pattern: new RegExp(jsdoc.source + prefixMatch.source + "public _" + varNameMatch.source + finalMatch.source),
      replacement: "",
      path: "plottable.d.ts",
    },
    header: {
      pattern: "VERSION",
      replacement: "<%= pkg.version %>",
      path: "license_header.tmp",
    },
    public_member_vars: {
      pattern: new RegExp(jsdoc.source + prefixMatch.source + "public " + "[^(;]*;"),
      replacement: "",
      path: "plottable.d.ts",
    },
  };

  var configJSON = {
    pkg: grunt.file.readJSON("package.json"),
    bump: bumpJSON,
    concat: {
      header: {
        src: ["license_header.tmp", "plottable.js"],
        dest: "plottable.js",
      },
    },
    ts: tsJSON,
    tslint: {
      options: {
        configuration: grunt.file.readJSON("tslint.json")
      },
      files: ["src/**/*.ts", "test/**.ts"]
    },
    watch: {
      "options": {
        livereload: true
      },
      "rebuild": {
        "tasks": ["dev-compile"],
        "files": ["src/**/*.ts"]
      },
      "tests": {
        "tasks": ["ts:test", "tslint"],
        "files": ["test/**.ts"]
      }
    },
    blanket_mocha: {
      all: ['test/coverage.html'],
      options: {
        threshold: 80
      }
    },
    connect: {
      server: {
        options: {
          port: 7007,
          livereload: true
        }
      }
    },
    clean: {tscommand: ["tscommand*.tmp.txt"], header: ["license_header.tmp"]},
    sed: sedJSON,
    copy: {
      dist: {
        files: [
          {src: "build/plottable.js",   dest:"plottable.js"            },
          {src: "build/plottable.d.ts", dest:"plottable.d.ts"          },
          {src: "build/tests.js",       dest: "test/tests.js"          },
          {src: "build/exampleUtil.js", dest: "examples/exampleUtil.js"}
        ]
      },
      header: {
        files: [{src: "license_header.txt", dest: "license_header.tmp"}]
      }
    },
    gitcommit: {
      version: {
        options: {
          message: "Release version <%= pkg.version %>"
        },
        files: {
          src: ['plottable.js', 'plottable.d.ts', 'examples/exampleUtil.js', 'test/tests.js', "package.json", "bower.json"]
        }
      },
      built: {
        options: {
          message: "Update built files"
        },
        files: {
          src: ['plottable.js', 'plottable.d.ts', 'examples/exampleUtil.js', 'test/tests.js']
        }
    }
    }
  };

  // project configuration
  grunt.initConfig(configJSON);

  require('load-grunt-tasks')(grunt);

  // default task (this is what runs when a task isn't specified)
  grunt.registerTask("handle-header",
            ["copy:header", "sed:header", "concat:header", "clean:header"]);
  grunt.registerTask("default", "launch");
  grunt.registerTask("dev-compile", [
                                  "ts:dev",
                                  "sed:private_definitions",
                                  "ts:test",
                                  "tslint",
                                  "clean:tscommand"]);
  grunt.registerTask("release:patch", ["bump:patch", "dist-compile", "gitcommit:version"]);
  grunt.registerTask("release:minor", ["bump:minor", "dist-compile", "gitcommit:version"]);
  grunt.registerTask("release:major", ["bump:major", "dist-compile", "gitcommit:version"]);

  grunt.registerTask("dist-compile", [
                                  "dev-compile",
                                  "blanket_mocha",
                                  "copy:dist",
                                  "handle-header",
                                  "sed:protected_definitions",
                                  "sed:public_member_vars"]);

  grunt.registerTask("commitjs", ["dist-compile", "gitcommit:built"]);

  grunt.registerTask("launch", ["connect", "dev-compile", "watch"]);
  grunt.registerTask("test", ["dev-compile", "blanket_mocha"]);
  grunt.registerTask("bm", ["blanket_mocha"]);
};
<commit_msg>Gruntfile without regular expression literals. Not working yet.<commit_after>/*
 * The Web project Gruntfile.
 *
 * Copyright 2013 Palantir Technologies, Inc. All rights reserved.
 */

module.exports = function(grunt) {
  "use strict";

  var path = require("path");
  var cwd = process.cwd();

  var tsJSON = {
    dev: {
      src: ["src/**/*.ts", "typings/**/*.d.ts"],
      out: "build/plottable.js",
      // watch: "src",
      options: {
        target: 'es5',
        noImplicitAny: true,
        sourceMap: false,
        declaration: true,
        removeComments: false
      }
    },
    test: {
      src: ["test/*.ts", "typings/**/*.d.ts", "build/plottable.d.ts"],
      out: "build/tests.js",
      // watch: "test",
      options: {
        target: 'es5',
        sourceMap: false,
        noImplicitAny: true,
        declaration: false,
        removeComments: false
      }
    },
    examples: {
      src: ["examples/*.ts", "typings/**/*.d.ts"],
      outDir: "build",
      // watch: "examples",
      options: {
        target: 'es5',
        sourceMap: false,
        declaration: false,
        removeComments: false
      }
    }
  };

  var bumpJSON = {
    options: {
      files: ['package.json', 'bower.json'],
      updateConfigs: ['pkg'],
      commit: false,
      createTag: false,
      push: false
    }
  }

  var prefixMatch = "\\n *";
  var varNameMatch = "[^(:;]*(\\([^)]*\\))?"; // catch function args too
  var nestedBraceMatch = ": \\{[^{}]*\\}";
  var typeNameMatch = ": [^;]*";
  var finalMatch = "((" + nestedBraceMatch + ")|(" + typeNameMatch + "))?;"
  var jsdoc_init = " *\\/\\*\\* *\\n";
  var jsdoc_mid = "( *\\*[^\\n]*\\n)+";
  var jsdoc_end = " *\\*\\/ *\\n";
  var jsdoc = "(" + jsdoc_init + jsdoc_mid + jsdoc_end + ")?";

  var sedJSON = {
    private_definitions: {
      pattern: jsdoc + prefixMatch + "private " + varNameMatch + finalMatch,
      replacement: "",
      path: "build/plottable.d.ts",
    },
    protected_definitions: {
      pattern: jsdoc + prefixMatch + "public _" + varNameMatch + finalMatch,
      replacement: "",
      path: "plottable.d.ts",
    },
    header: {
      pattern: "VERSION",
      replacement: "<%= pkg.version %>",
      path: "license_header.tmp",
    },
    public_member_vars: {
      pattern: jsdoc + prefixMatch + "public " + "[^(;]*;",
      replacement: "",
      path: "plottable.d.ts",
    },
  };

  var configJSON = {
    pkg: grunt.file.readJSON("package.json"),
    bump: bumpJSON,
    concat: {
      header: {
        src: ["license_header.tmp", "plottable.js"],
        dest: "plottable.js",
      },
    },
    ts: tsJSON,
    tslint: {
      options: {
        configuration: grunt.file.readJSON("tslint.json")
      },
      files: ["src/**/*.ts", "test/**.ts"]
    },
    watch: {
      "options": {
        livereload: true
      },
      "rebuild": {
        "tasks": ["dev-compile"],
        "files": ["src/**/*.ts"]
      },
      "tests": {
        "tasks": ["ts:test", "tslint"],
        "files": ["test/**.ts"]
      }
    },
    blanket_mocha: {
      all: ['test/coverage.html'],
      options: {
        threshold: 80
      }
    },
    connect: {
      server: {
        options: {
          port: 7007,
          livereload: true
        }
      }
    },
    clean: {tscommand: ["tscommand*.tmp.txt"], header: ["license_header.tmp"]},
    sed: sedJSON,
    copy: {
      dist: {
        files: [
          {src: "build/plottable.js",   dest:"plottable.js"            },
          {src: "build/plottable.d.ts", dest:"plottable.d.ts"          },
          {src: "build/tests.js",       dest: "test/tests.js"          },
          {src: "build/exampleUtil.js", dest: "examples/exampleUtil.js"}
        ]
      },
      header: {
        files: [{src: "license_header.txt", dest: "license_header.tmp"}]
      }
    },
    gitcommit: {
      version: {
        options: {
          message: "Release version <%= pkg.version %>"
        },
        files: {
          src: ['plottable.js', 'plottable.d.ts', 'examples/exampleUtil.js', 'test/tests.js', "package.json", "bower.json"]
        }
      },
      built: {
        options: {
          message: "Update built files"
        },
        files: {
          src: ['plottable.js', 'plottable.d.ts', 'examples/exampleUtil.js', 'test/tests.js']
        }
    }
    }
  };

  // project configuration
  grunt.initConfig(configJSON);

  require('load-grunt-tasks')(grunt);

  // default task (this is what runs when a task isn't specified)
  grunt.registerTask("handle-header",
            ["copy:header", "sed:header", "concat:header", "clean:header"]);
  grunt.registerTask("default", "launch");
  grunt.registerTask("dev-compile", [
                                  "ts:dev",
                                  "sed:private_definitions",
                                  "ts:test",
                                  "tslint",
                                  "clean:tscommand"]);
  grunt.registerTask("release:patch", ["bump:patch", "dist-compile", "gitcommit:version"]);
  grunt.registerTask("release:minor", ["bump:minor", "dist-compile", "gitcommit:version"]);
  grunt.registerTask("release:major", ["bump:major", "dist-compile", "gitcommit:version"]);

  grunt.registerTask("dist-compile", [
                                  "dev-compile",
                                  "blanket_mocha",
                                  "copy:dist",
                                  "handle-header",
                                  "sed:protected_definitions",
                                  "sed:public_member_vars"]);

  grunt.registerTask("commitjs", ["dist-compile", "gitcommit:built"]);

  grunt.registerTask("launch", ["connect", "dev-compile", "watch"]);
  grunt.registerTask("test", ["dev-compile", "blanket_mocha"]);
  grunt.registerTask("bm", ["blanket_mocha"]);
};
<|endoftext|>


<commit_before>define("DrawEngine", ["Vector"], function (vector) {
    function DrawEngine(arenaDisplay, arena) {
        this.arena = arena;
        this.display = arenaDisplay.getContext("2d");

        var size = { width: arenaDisplay.width, height: arenaDisplay.height };
        var lastTime = 0;
        var speed = 100;

        var self = this;

        var update = function (time) {
            if (lastTime === 0) lastTime = time;
            self.display.clearRect(0, 0, size.width, size.height);

            var deltaT = time - lastTime;

            for (var i = 0; i < self.arena.players.length; i++) {
                var player = self.arena.players[i].Token;
                movePlayerToNewPosition(player, deltaT);
                paintPlayer(player.CurrentPosition, player.Size, player.MovePath.slice(player.CurrentIndex));
            }

            lastTime = time;
            requestAnimationFrame(update);
        };

        var movePlayerToNewPosition = function (player, deltaT) {
            if (player.CurrentPosition === undefined) {
                player.CurrentPosition = player.Position;
                player.CurrentIndex = 0;
            }

            var endPoint = player.MovePath[player.CurrentIndex];
            if (endPoint == undefined)
                return;

            var distance = vectorRemove(endPoint, player.CurrentPosition);
            var moveDistance = calculateMoveDistance(distance, deltaT);

            player.CurrentPosition = vectorAdd(player.CurrentPosition, moveDistance);

            if (hasReachedEnd(distance, player.CurrentPosition, endPoint)) {
                pointPlayerToNextTarget(player, endPoint);
            }

        }

        var calculateMoveDistance = function (distance, deltaT) {
            var moveLength = vectorLength(distance);

            var sin = distance.Y / moveLength;
            var cos = distance.X / moveLength;

            return {
                X: speed * deltaT * cos / 1000,
                Y: speed * deltaT * sin / 1000
            };
        };

        var hasReachedEnd = function (distance, currentPosition, endPoint) {
            return distance.X >= 0 && currentPosition.X >= endPoint.X ||
                   distance.X <= 0 && currentPosition.X <= endPoint.X ||
                   distance.Y >= 0 && currentPosition.Y >= endPoint.Y ||
                   distance.Y <= 0 && currentPosition.Y <= endPoint.Y;
        };

        var pointPlayerToNextTarget = function (player, endPoint) {
            player.CurrentPosition.X = endPoint.X;
            player.CurrentPosition.Y = endPoint.Y;
            while (endPoint && vectorsAreEqual(player.CurrentPosition, endPoint)) {
                player.CurrentIndex++;
                endPoint = player.MovePath[player.CurrentIndex];
            }
        };

        var paintPlayer = function (position, size, movePath) {
            self.display.fillRect(position.X, position.Y, size.X, size.Y);
            self.display.strokeStyle = "#777";
            self.display.beginPath();
            self.display.moveTo(position.X, position.Y);
            for (var j = 0; j < movePath.length; j++) {
                var pathPoint = movePath[j];
                self.display.lineTo(pathPoint.X, pathPoint.Y);
            }
            self.display.stroke();
        };

        requestAnimationFrame(update);
    }

    return DrawEngine;
});<commit_msg>moved common part of calculation to variable<commit_after>define("DrawEngine", ["Vector"], function (vector) {
    function DrawEngine(arenaDisplay, arena) {
        this.arena = arena;
        this.display = arenaDisplay.getContext("2d");

        var size = { width: arenaDisplay.width, height: arenaDisplay.height };
        var lastTime = 0;
        var speed = 100;

        var self = this;

        var update = function (time) {
            if (lastTime === 0) lastTime = time;
            self.display.clearRect(0, 0, size.width, size.height);

            var deltaT = time - lastTime;

            for (var i = 0; i < self.arena.players.length; i++) {
                var player = self.arena.players[i].Token;
                movePlayerToNewPosition(player, deltaT);
                paintPlayer(player.CurrentPosition, player.Size, player.MovePath.slice(player.CurrentIndex));
            }

            lastTime = time;
            requestAnimationFrame(update);
        };

        var movePlayerToNewPosition = function (player, deltaT) {
            if (player.CurrentPosition === undefined) {
                player.CurrentPosition = player.Position;
                player.CurrentIndex = 0;
            }

            var endPoint = player.MovePath[player.CurrentIndex];
            if (endPoint == undefined)
                return;

            var distance = vectorRemove(endPoint, player.CurrentPosition);
            var moveDistance = calculateMoveDistance(distance, deltaT);

            player.CurrentPosition = vectorAdd(player.CurrentPosition, moveDistance);

            if (hasReachedEnd(distance, player.CurrentPosition, endPoint)) {
                pointPlayerToNextTarget(player, endPoint);
            }
        }

        var calculateMoveDistance = function (distance, deltaT) {
            var moveLength = vectorLength(distance);
            var sin = distance.Y / moveLength;
            var cos = distance.X / moveLength;
            var normalSpeed = speed * deltaT / 1000;

            return {
                X: cos * normalSpeed,
                Y: sin * normalSpeed
            };
        };

        var hasReachedEnd = function (distance, currentPosition, endPoint) {
            return distance.X >= 0 && currentPosition.X >= endPoint.X ||
                   distance.X <= 0 && currentPosition.X <= endPoint.X ||
                   distance.Y >= 0 && currentPosition.Y >= endPoint.Y ||
                   distance.Y <= 0 && currentPosition.Y <= endPoint.Y;
        };

        var pointPlayerToNextTarget = function (player, endPoint) {
            player.CurrentPosition.X = endPoint.X;
            player.CurrentPosition.Y = endPoint.Y;
            while (endPoint && vectorsAreEqual(player.CurrentPosition, endPoint)) {
                player.CurrentIndex++;
                endPoint = player.MovePath[player.CurrentIndex];
            }
        };

        var paintPlayer = function (position, size, movePath) {
            self.display.fillRect(position.X, position.Y, size.X, size.Y);
            self.display.strokeStyle = "#777";
            self.display.beginPath();
            self.display.moveTo(position.X, position.Y);
            for (var j = 0; j < movePath.length; j++) {
                var pathPoint = movePath[j];
                self.display.lineTo(pathPoint.X, pathPoint.Y);
            }
            self.display.stroke();
        };

        requestAnimationFrame(update);
    }

    return DrawEngine;
});<|endoftext|>


<commit_before>'use strict';

const testing = require('testing');
const Koa = require('koa');
const koaRouter = require('koa-router');
const rq = require('request-promise');

module.exports.test = callback => {
	let tests = {};
	let routesTest = require('../app/routes/koas-router');
	let controllerTest = require('../app/controllers/controller');
	let Koax = require('../app/action/action');
	let Koas = require('../index');
	//koas-router 测试
	tests.routes = callback =>{
		let koasRoutes = new routesTest(true);
		koasRoutes.map = 1;//对set map()进行测试
		koasRoutes.jsonMap = 1;//对set jsonMap()进行测试
		testing.verify(Array.isArray(koasRoutes.map),'koasRoutes.map must be an Array',callback);
		testing.verify(typeof koasRoutes.jsonMap === 'object','koasRoutes.jsonMap must be an Object',callback);
		console.log(koasRoutes.jsonMap,koasRoutes.map)//测试的时候看结构用
		koasRoutes.deleteRouter('list','listtest1');
		let testMapDel = () => {
			for(let i = 0;i < koasRoutes.map.length;i++){
				if(koasRoutes.map[i] =='')
					return true
			}
			return false;
		}
		testing.verify(koasRoutes.jsonMap.list.listtest1.status==0,'delete failed ,please check deleteRouter',callback);
		testing.verify(testMapDel(),'delete failed ,please check deleteRouter',callback)
		koasRoutes.addRouter('list','listtest1');
		let testMapAdd = () => {
			for(let i = 0;i < koasRoutes.map.length;i++){
				if(koasRoutes.map[i] =='')
					return false
			}
			return true;
		}
		testing.verify(koasRoutes.jsonMap.list.listtest1.status==1,'add failed ,please check addRouter',callback);
		testing.verify(testMapAdd(),'add failed ,please check addRouter',callback)
		testing.success(callback);
	}
	tests.controller = callback => {
		let koasController = new controllerTest(true);
		koasController.jsonMap = 1;//对set jsonMap()进行测试
		testing.verify(typeof koasController.jsonMap === 'object','koasController.map must be an Object',callback)
		console.log(koasController.jsonMap);
		testing.success(callback);
		let koasSlot = koasController.slot('movie','movietest1')
		testing.verify((typeof koasSlot).match('function'),'slot must return a function or async function',callback)
	}
	tests.Koas = async callback => {
		let app = new Koas(true);
		let koax = new Koax();
		let testkoa = new Koa();
		let testrouter = new koaRouter();
		testrouter.get('/testkoax1',(ctx,next)=>{
			ctx.body = require('./json/testKoax1');
		});
		testrouter.post('/testkoax2',(ctx,next)=>{
			ctx.body = require('./json/testKoax2');
		});
		testkoa.use(testrouter.routes());
		let testserver = testkoa.listen('8012');
		koax.setName('testKoax1').request({
			uri:'http://localhost:8012/testkoax1',
			method:'GET'
		});
		koax.setName('testKoax2').request({
			uri:'http://localhost:8012/testKoax2',
			method:'POST'
		});
		app.use(koax.middleware());
		let server = app.listen('8011');
		let httprequest = await rq('http://localhost:8011/list');
		testing.verify(httprequest,callback);
		testserver.close((error) => {
			testing.check(error, 'Could not stop server', callback);
		});
		server.close((error) => {
			testing.check(error, 'Could not stop server', callback);
			testing.success(callback);
		});
	}
	testing.run(tests, 1000, callback);
}
exports.test(testing.show);<commit_msg>change test<commit_after>'use strict';

const testing = require('testing');
const Koa = require('koa');
const koaRouter = require('koa-router');
const rq = require('request-promise');

module.exports.test = callback => {
	let tests = {};
	let routesTest = require('../app/routes/koas-router');
	let controllerTest = require('../app/controllers/controller');
	let Koax = require('../app/action/action');
	let Koas = require('../index');
	//koas-router 测试
	tests.routes = callback =>{
		let koasRoutes = new routesTest(true);
		koasRoutes.map = 1;//对set map()进行测试
		koasRoutes.jsonMap = 1;//对set jsonMap()进行测试
		testing.verify(Array.isArray(koasRoutes.map),'koasRoutes.map must be an Array',callback);
		testing.verify(typeof koasRoutes.jsonMap === 'object','koasRoutes.jsonMap must be an Object',callback);
		console.log(koasRoutes.jsonMap,koasRoutes.map)//测试的时候看结构用
		koasRoutes.deleteRouter('list','listtest1');
		let testMapDel = () => {
			for(let i = 0;i < koasRoutes.map.length;i++){
				if(koasRoutes.map[i] =='')
					return true
			}
			return false;
		}
		testing.verify(koasRoutes.jsonMap.list.listtest1.status==0,'delete failed ,please check deleteRouter',callback);
		testing.verify(testMapDel(),'delete failed ,please check deleteRouter',callback)
		koasRoutes.addRouter('list','listtest1');
		let testMapAdd = () => {
			for(let i = 0;i < koasRoutes.map.length;i++){
				if(koasRoutes.map[i] =='')
					return false
			}
			return true;
		}
		testing.verify(koasRoutes.jsonMap.list.listtest1.status==1,'add failed ,please check addRouter',callback);
		testing.verify(testMapAdd(),'add failed ,please check addRouter',callback)
		testing.success(callback);
	}
	tests.controller = callback => {
		let koasController = new controllerTest(true);
		koasController.jsonMap = 1;//对set jsonMap()进行测试
		testing.verify(typeof koasController.jsonMap === 'object','koasController.map must be an Object',callback)
		console.log(koasController.jsonMap);
		testing.success(callback);
		let koasSlot = koasController.slot('movie','movietest1')
		testing.verify((typeof koasSlot).match('function'),'slot must return a function or async function',callback)
	}
	tests.Koas = async callback => {
		let app = new Koas(true);
		let koax = new Koax();
		let testkoa = new Koa();
		let testrouter = new koaRouter();
		testrouter.get('/testkoax1',(ctx,next)=>{
			ctx.body = require('./json/testKoax1');
			ctx.status = 200;
		});
		testrouter.post('/testkoax2',(ctx,next)=>{
			ctx.body = require('./json/testKoax2');
			ctx.status = 200;
		});
		testkoa.use(testrouter.routes());
		let testserver = testkoa.listen('8012');
		koax.setName('testKoax1').request({
			uri:'http://localhost:8012/testkoax1',
			method:'GET'
		});
		koax.setName('testKoax2').cached().request({
			uri:'http://localhost:8012/testKoax2',
			method:'POST'
		});
		app.use(koax.middleware());
		let server = app.listen('8011');
		let httprequest = await rq('http://localhost:8011/list');
		testing.verify(httprequest,callback);
		testserver.close((error) => {
			testing.check(error, 'Could not stop server', callback);
		});
		// server.close((error) => {
		// 	testing.check(error, 'Could not stop server', callback);
		// 	testing.success(callback);
		// });
		testing.success(callback);
	}
	testing.run(tests, 1000, callback);
}
exports.test(testing.show);<|endoftext|>


<commit_before>var dl = require('datalib'),
    template = dl.template,
    expr = require('vega-expression'),
    args = ['datum', 'event', 'signals'];

var compile = expr.compiler(args, {
  idWhiteList: args,
  fieldVar:    args[0],
  globalVar:   function(id) {
    return 'this.sig[' + dl.str(id) + ']._value';
  },
  functions:   function(codegen) {
    var fn = expr.functions(codegen);
    fn.eventItem  = 'event.vg.getItem';
    fn.eventGroup = 'event.vg.getGroup';
    fn.eventX     = 'event.vg.getX';
    fn.eventY     = 'event.vg.getY';
    fn.open       = 'window.open';
    fn.scale      = scaleGen(codegen, false);
    fn.iscale     = scaleGen(codegen, true);
    fn.inrange    = 'this.defs.inrange';
    fn.format     = 'this.defs.format';
    fn.timeFormat = 'this.defs.timeFormat';
    fn.utcFormat  = 'this.defs.utcFormat';
    return fn;
  },
  functionDefs: function(/*codegen*/) {
    return {
      'scale':      scale,
      'inrange':    inrange,
      'format':     numberFormat,
      'timeFormat': timeFormat,
      'utcFormat':  utcFormat
    };
  }
});

function scaleGen(codegen, invert) {
  return function(args) {
    args = args.map(codegen);
    var n = args.length;
    if (n < 2 || n > 3) {
      throw Error("scale takes exactly 2 or 3 arguments.");
    }
    return 'this.defs.scale(this.model, ' + invert + ', ' +
      args[0] + ',' + args[1] + (n > 2 ? ',' + args[2] : '') + ')';
  };
}

function scale(model, invert, name, value, scope) {
  if (!scope || !scope.scale) {
    scope = (scope && scope.mark) ? scope.mark.group : model.scene().items[0];
  }
  // Verify scope is valid
  if (model.group(scope._id) !== scope) {
    throw Error('Scope for scale "'+name+'" is not a valid group item.');
  }
  var s = scope.scale(name);
  return !s ? value : (invert ? s.invert(value) : s(value));
}

function inrange(val, a, b, exclusive) {
  var min = a, max = b;
  if (a > b) { min = b; max = a; }
  return exclusive ?
    (min < val && max > val) :
    (min <= val && max >= val);
}

function numberFormat(specifier, v) {
  return template.format(specifier, 'number')(v);
}

function timeFormat(specifier, d) {
  return template.format(specifier, 'time')(typeof d==='number' ? new Date(d) : d);
}

function utcFormat(specifier, d) {
  return template.format(specifier, 'utc')(typeof d==='number' ? new Date(d) : d);
}

function wrap(model) {
  return function(str) {
    var x = compile(str);
    x.model = model;
    x.sig = model ? model._signals : {};
    return x;
  };
}
wrap.codegen = compile.codegen;
wrap.scale = scale;
module.exports = wrap;
<commit_msg>Don’t need to convert data to date time before formatting. <commit_after>var dl = require('datalib'),
    template = dl.template,
    expr = require('vega-expression'),
    args = ['datum', 'event', 'signals'];

var compile = expr.compiler(args, {
  idWhiteList: args,
  fieldVar:    args[0],
  globalVar:   function(id) {
    return 'this.sig[' + dl.str(id) + ']._value';
  },
  functions:   function(codegen) {
    var fn = expr.functions(codegen);
    fn.eventItem  = 'event.vg.getItem';
    fn.eventGroup = 'event.vg.getGroup';
    fn.eventX     = 'event.vg.getX';
    fn.eventY     = 'event.vg.getY';
    fn.open       = 'window.open';
    fn.scale      = scaleGen(codegen, false);
    fn.iscale     = scaleGen(codegen, true);
    fn.inrange    = 'this.defs.inrange';
    fn.format     = 'this.defs.format';
    fn.timeFormat = 'this.defs.timeFormat';
    fn.utcFormat  = 'this.defs.utcFormat';
    return fn;
  },
  functionDefs: function(/*codegen*/) {
    return {
      'scale':      scale,
      'inrange':    inrange,
      'format':     numberFormat,
      'timeFormat': timeFormat,
      'utcFormat':  utcFormat
    };
  }
});

function scaleGen(codegen, invert) {
  return function(args) {
    args = args.map(codegen);
    var n = args.length;
    if (n < 2 || n > 3) {
      throw Error("scale takes exactly 2 or 3 arguments.");
    }
    return 'this.defs.scale(this.model, ' + invert + ', ' +
      args[0] + ',' + args[1] + (n > 2 ? ',' + args[2] : '') + ')';
  };
}

function scale(model, invert, name, value, scope) {
  if (!scope || !scope.scale) {
    scope = (scope && scope.mark) ? scope.mark.group : model.scene().items[0];
  }
  // Verify scope is valid
  if (model.group(scope._id) !== scope) {
    throw Error('Scope for scale "'+name+'" is not a valid group item.');
  }
  var s = scope.scale(name);
  return !s ? value : (invert ? s.invert(value) : s(value));
}

function inrange(val, a, b, exclusive) {
  var min = a, max = b;
  if (a > b) { min = b; max = a; }
  return exclusive ?
    (min < val && max > val) :
    (min <= val && max >= val);
}

function numberFormat(specifier, v) {
  return template.format(specifier, 'number')(v);
}

function timeFormat(specifier, d) {
  return template.format(specifier, 'time')(d);
}

function utcFormat(specifier, d) {
  return template.format(specifier, 'utc')(d);
}

function wrap(model) {
  return function(str) {
    var x = compile(str);
    x.model = model;
    x.sig = model ? model._signals : {};
    return x;
  };
}
wrap.codegen = compile.codegen;
wrap.scale = scale;
module.exports = wrap;
<|endoftext|>