// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.ui.SliderBaseTest');
goog.setTestOnly('goog.ui.SliderBaseTest');

goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.State');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classlist');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.fx.Animation');
goog.require('goog.math.Coordinate');
goog.require('goog.style');
goog.require('goog.style.bidi');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.mockmatchers');
goog.require('goog.testing.recordFunction');
goog.require('goog.ui.Component');
goog.require('goog.ui.SliderBase');
goog.require('goog.userAgent');

var oneThumbSlider;
var oneThumbSliderRtl;
var oneChangeEventCount;

var twoThumbSlider;
var twoThumbSliderRtl;
var twoChangeEventCount;

var mockClock;
var mockAnimation;



/**
 * A basic class to implement the abstract goog.ui.SliderBase for testing.
 * @param {boolean=} testOnlyIsRightToLeft This parameter is necessary to tell
 *     if the slider is rendered right-to-left when creating thumbs (before
 *     entering the document). Used only for test purposes.
 * @constructor
 * @extends {goog.ui.SliderBase}
 */
function OneThumbSlider(testOnlyIsRightToLeft = false) {
  this.testOnlyIsRightToLeft_ = testOnlyIsRightToLeft;
  goog.ui.SliderBase.call(this, undefined /* domHelper */, function(value) {
    return value > 5 ? 'A big value.' : 'A small value.';
  });
}
goog.inherits(OneThumbSlider, goog.ui.SliderBase);


/** @override */
OneThumbSlider.prototype.createThumbs = function() {
  var dirSuffix = this.testOnlyIsRightToLeft_ ? 'Rtl' : '';
  this.valueThumb = this.extentThumb = goog.dom.getElement('thumb' + dirSuffix);
};


/** @override */
OneThumbSlider.prototype.getCssClass = function(orientation) {
  return goog.getCssName('test-slider', orientation);
};



/**
 * A basic class to implement the abstract goog.ui.SliderBase for testing.
 * @param {boolean=} testOnlyIsRightToLeft This parameter is necessary to tell
 *     if the slider is rendered right-to-left when creating thumbs (before
 *     entering the document). Used only for test purposes.
 * @constructor
 * @extends {goog.ui.SliderBase}
 */
function TwoThumbSlider(testOnlyIsRightToLeft = false) {
  this.testOnlyIsRightToLeft_ = testOnlyIsRightToLeft;
  goog.ui.SliderBase.call(this);
}
goog.inherits(TwoThumbSlider, goog.ui.SliderBase);


/** @override */
TwoThumbSlider.prototype.createThumbs = function() {
  var dirSuffix = this.testOnlyIsRightToLeft_ ? 'Rtl' : '';
  this.valueThumb = goog.dom.getElement('valueThumb' + dirSuffix);
  this.extentThumb = goog.dom.getElement('extentThumb' + dirSuffix);
  this.rangeHighlight = goog.dom.getElement('rangeHighlight' + dirSuffix);
};


/** @override */
TwoThumbSlider.prototype.getCssClass = function(orientation) {
  return goog.getCssName('test-slider', orientation);
};



/**
 * Basic class that implements the AnimationFactory interface for testing.
 * @param {!goog.fx.Animation|!Array<!goog.fx.Animation>} testAnimations The
 *     test animations to use.
 * @constructor
 * @implements {goog.ui.SliderBase.AnimationFactory}
 */
function AnimationFactory(testAnimations) {
  this.testAnimations = testAnimations;
}


/** @override */
AnimationFactory.prototype.createAnimations = function() {
  return this.testAnimations;
};


function setUp() {
  var sandBox = goog.dom.getElement('sandbox');
  mockClock = new goog.testing.MockClock(true);

  var oneThumbElem = goog.dom.createDom(
      goog.dom.TagName.DIV, {'id': 'oneThumbSlider'},
      goog.dom.createDom(goog.dom.TagName.SPAN, {'id': 'thumb'}));
  sandBox.appendChild(oneThumbElem);
  oneThumbSlider = new OneThumbSlider();
  oneThumbSlider.decorate(oneThumbElem);
  oneChangeEventCount = 0;
  goog.events.listen(
      oneThumbSlider, goog.ui.Component.EventType.CHANGE,
      function() { oneChangeEventCount++; });

  var twoThumbElem = goog.dom.createDom(
      goog.dom.TagName.DIV, {'id': 'twoThumbSlider'},
      goog.dom.createDom(goog.dom.TagName.DIV, {'id': 'rangeHighlight'}),
      goog.dom.createDom(goog.dom.TagName.SPAN, {'id': 'valueThumb'}),
      goog.dom.createDom(goog.dom.TagName.SPAN, {'id': 'extentThumb'}));
  sandBox.appendChild(twoThumbElem);
  twoThumbSlider = new TwoThumbSlider();
  twoThumbSlider.decorate(twoThumbElem);
  twoChangeEventCount = 0;
  goog.events.listen(
      twoThumbSlider, goog.ui.Component.EventType.CHANGE,
      function() { twoChangeEventCount++; });

  var sandBoxRtl = goog.dom.createDom(
      goog.dom.TagName.DIV, {'dir': 'rtl', 'style': 'position:absolute;'});
  sandBox.appendChild(sandBoxRtl);

  var oneThumbElemRtl = goog.dom.createDom(
      goog.dom.TagName.DIV, {'id': 'oneThumbSliderRtl'},
      goog.dom.createDom(goog.dom.TagName.SPAN, {'id': 'thumbRtl'}));
  sandBoxRtl.appendChild(oneThumbElemRtl);
  oneThumbSliderRtl = new OneThumbSlider(true /* testOnlyIsRightToLeft */);
  oneThumbSliderRtl.enableFlipForRtl(true);
  oneThumbSliderRtl.decorate(oneThumbElemRtl);
  goog.events.listen(
      oneThumbSliderRtl, goog.ui.Component.EventType.CHANGE,
      function() { oneChangeEventCount++; });

  var twoThumbElemRtl = goog.dom.createDom(
      goog.dom.TagName.DIV, {'id': 'twoThumbSliderRtl'},
      goog.dom.createDom(goog.dom.TagName.DIV, {'id': 'rangeHighlightRtl'}),
      goog.dom.createDom(goog.dom.TagName.SPAN, {'id': 'valueThumbRtl'}),
      goog.dom.createDom(goog.dom.TagName.SPAN, {'id': 'extentThumbRtl'}));
  sandBoxRtl.appendChild(twoThumbElemRtl);
  twoThumbSliderRtl = new TwoThumbSlider(true /* testOnlyIsRightToLeft */);
  twoThumbSliderRtl.enableFlipForRtl(true);
  twoThumbSliderRtl.decorate(twoThumbElemRtl);
  twoChangeEventCount = 0;
  goog.events.listen(
      twoThumbSliderRtl, goog.ui.Component.EventType.CHANGE,
      function() { twoChangeEventCount++; });
}

function tearDown() {
  oneThumbSlider.dispose();
  twoThumbSlider.dispose();
  oneThumbSliderRtl.dispose();
  twoThumbSliderRtl.dispose();
  mockClock.dispose();
  goog.dom.removeChildren(goog.dom.getElement('sandbox'));
}

function testGetAndSetValue() {
  oneThumbSlider.setValue(30);
  assertEquals(30, oneThumbSlider.getValue());
  assertEquals(
      'Setting valid value must dispatch only a single change event.', 1,
      oneChangeEventCount);

  oneThumbSlider.setValue(30);
  assertEquals(30, oneThumbSlider.getValue());
  assertEquals(
      'Setting to same value must not dispatch change event.', 1,
      oneChangeEventCount);

  oneThumbSlider.setValue(-30);
  assertEquals(
      'Setting invalid value must not change value.', 30,
      oneThumbSlider.getValue());
  assertEquals(
      'Setting invalid value must not dispatch change event.', 1,
      oneChangeEventCount);


  // Value thumb can't go past extent thumb, so we must move that first to
  // allow setting value.
  twoThumbSlider.setExtent(70);
  twoChangeEventCount = 0;
  twoThumbSlider.setValue(60);
  assertEquals(60, twoThumbSlider.getValue());
  assertEquals(
      'Setting valid value must dispatch only a single change event.', 1,
      twoChangeEventCount);

  twoThumbSlider.setValue(60);
  assertEquals(60, twoThumbSlider.getValue());
  assertEquals(
      'Setting to same value must not dispatch change event.', 1,
      twoChangeEventCount);

  twoThumbSlider.setValue(-60);
  assertEquals(
      'Setting invalid value must not change value.', 60,
      twoThumbSlider.getValue());
  assertEquals(
      'Setting invalid value must not dispatch change event.', 1,
      twoChangeEventCount);
}

function testGetAndSetValueRtl() {
  var thumbElement = goog.dom.getElement('thumbRtl');
  assertEquals(0, goog.style.bidi.getOffsetStart(thumbElement));
  assertEquals('', thumbElement.style.left);
  assertEquals('0px', thumbElement.style.right);

  oneThumbSliderRtl.setValue(30);
  assertEquals(30, oneThumbSliderRtl.getValue());
  assertEquals(
      'Setting valid value must dispatch only a single change event.', 1,
      oneChangeEventCount);

  assertEquals('', thumbElement.style.left);
  assertEquals('294px', thumbElement.style.right);

  oneThumbSliderRtl.setValue(30);
  assertEquals(30, oneThumbSliderRtl.getValue());
  assertEquals(
      'Setting to same value must not dispatch change event.', 1,
      oneChangeEventCount);

  oneThumbSliderRtl.setValue(-30);
  assertEquals(
      'Setting invalid value must not change value.', 30,
      oneThumbSliderRtl.getValue());
  assertEquals(
      'Setting invalid value must not dispatch change event.', 1,
      oneChangeEventCount);


  // Value thumb can't go past extent thumb, so we must move that first to
  // allow setting value.
  var valueThumbElement = goog.dom.getElement('valueThumbRtl');
  var extentThumbElement = goog.dom.getElement('extentThumbRtl');
  assertEquals(0, goog.style.bidi.getOffsetStart(valueThumbElement));
  assertEquals(0, goog.style.bidi.getOffsetStart(extentThumbElement));
  assertEquals('', valueThumbElement.style.left);
  assertEquals('0px', valueThumbElement.style.right);
  assertEquals('', extentThumbElement.style.left);
  assertEquals('0px', extentThumbElement.style.right);

  twoThumbSliderRtl.setExtent(70);
  twoChangeEventCount = 0;
  twoThumbSliderRtl.setValue(60);
  assertEquals(60, twoThumbSliderRtl.getValue());
  assertEquals(
      'Setting valid value must dispatch only a single change event.', 1,
      twoChangeEventCount);

  twoThumbSliderRtl.setValue(60);
  assertEquals(60, twoThumbSliderRtl.getValue());
  assertEquals(
      'Setting to same value must not dispatch change event.', 1,
      twoChangeEventCount);

  assertEquals('', valueThumbElement.style.left);
  assertEquals('600px', valueThumbElement.style.right);
  assertEquals('', extentThumbElement.style.left);
  assertEquals('700px', extentThumbElement.style.right);

  twoThumbSliderRtl.setValue(-60);
  assertEquals(
      'Setting invalid value must not change value.', 60,
      twoThumbSliderRtl.getValue());
  assertEquals(
      'Setting invalid value must not dispatch change event.', 1,
      twoChangeEventCount);
}

function testGetAndSetExtent() {
  // Note(user): With a one thumb slider the API only really makes sense if you
  // always use setValue since there is no extent.

  twoThumbSlider.setExtent(7);
  assertEquals(7, twoThumbSlider.getExtent());
  assertEquals(
      'Setting valid value must dispatch only a single change event.', 1,
      twoChangeEventCount);

  twoThumbSlider.setExtent(7);
  assertEquals(7, twoThumbSlider.getExtent());
  assertEquals(
      'Setting to same value must not dispatch change event.', 1,
      twoChangeEventCount);

  twoThumbSlider.setExtent(-7);
  assertEquals(
      'Setting invalid value must not change value.', 7,
      twoThumbSlider.getExtent());
  assertEquals(
      'Setting invalid value must not dispatch change event.', 1,
      twoChangeEventCount);
}

function testUpdateValueExtent() {
  twoThumbSlider.setValueAndExtent(30, 50);

  assertNotNull(twoThumbSlider.getElement());
  assertEquals(
      'Setting value results in updating aria-valuenow', '30',
      goog.a11y.aria.getState(
          twoThumbSlider.getElement(), goog.a11y.aria.State.VALUENOW));
  assertEquals(30, twoThumbSlider.getValue());
  assertEquals(50, twoThumbSlider.getExtent());
}

function testValueText() {
  oneThumbSlider.setValue(10);
  assertEquals(
      'Setting value results in correct aria-valuetext', 'A big value.',
      goog.a11y.aria.getState(
          oneThumbSlider.getElement(), goog.a11y.aria.State.VALUETEXT));
  oneThumbSlider.setValue(2);
  assertEquals(
      'Updating value results in updated aria-valuetext', 'A small value.',
      goog.a11y.aria.getState(
          oneThumbSlider.getElement(), goog.a11y.aria.State.VALUETEXT));
}

function testGetValueText() {
  oneThumbSlider.setValue(10);
  assertEquals(
      'Getting the text value gets the correct description', 'A big value.',
      oneThumbSlider.getTextValue());
  oneThumbSlider.setValue(2);
  assertEquals(
      'Getting the updated text value gets the correct updated description',
      'A small value.', oneThumbSlider.getTextValue());
}

function testRangeListener() {
  var slider = new goog.ui.SliderBase;
  slider.updateUi_ = slider.updateAriaStates = function() {};
  slider.rangeModel.setValue(0);

  var f = goog.testing.recordFunction();
  goog.events.listen(slider, goog.ui.Component.EventType.CHANGE, f);

  slider.rangeModel.setValue(50);
  assertEquals(1, f.getCallCount());

  slider.exitDocument();
  slider.rangeModel.setValue(0);
  assertEquals(
      'The range model listener should not have been removed so we ' +
          'should have gotten a second event dispatch',
      2, f.getCallCount());
}


/**
 * Verifies that rangeHighlight position and size are correct for the given
 * startValue and endValue. Assumes slider has default min/max values [0, 100],
 * width of 1020px, and thumb widths of 20px, with rangeHighlight drawn from
 * the centers of the thumbs.
 * @param {number} rangeHighlight The range highlight.
 * @param {number} startValue The start value.
 * @param {number} endValue The end value.
 */
function assertHighlightedRange(rangeHighlight, startValue, endValue) {
  var rangeStr = '[' + startValue + ', ' + endValue + ']';
  var rangeStart = 10 + 10 * startValue;
  assertEquals(
      'Range highlight for ' + rangeStr + ' should start at ' + rangeStart +
          'px.',
      rangeStart, rangeHighlight.offsetLeft);
  var rangeSize = 10 * (endValue - startValue);
  assertEquals(
      'Range highlight for ' + rangeStr + ' should have size ' + rangeSize +
          'px.',
      rangeSize, rangeHighlight.offsetWidth);
}

function testKeyHandlingTests() {
  twoThumbSlider.setValue(0);
  twoThumbSlider.setExtent(100);
  assertEquals(0, twoThumbSlider.getValue());
  assertEquals(100, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.RIGHT);
  assertEquals(1, twoThumbSlider.getValue());
  assertEquals(99, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.RIGHT);
  assertEquals(2, twoThumbSlider.getValue());
  assertEquals(98, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.LEFT);
  assertEquals(1, twoThumbSlider.getValue());
  assertEquals(98, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.LEFT);
  assertEquals(0, twoThumbSlider.getValue());
  assertEquals(98, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.RIGHT,
      {shiftKey: true});
  assertEquals(10, twoThumbSlider.getValue());
  assertEquals(90, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.RIGHT,
      {shiftKey: true});
  assertEquals(20, twoThumbSlider.getValue());
  assertEquals(80, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.LEFT, {shiftKey: true});
  assertEquals(10, twoThumbSlider.getValue());
  assertEquals(80, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.LEFT, {shiftKey: true});
  assertEquals(0, twoThumbSlider.getValue());
  assertEquals(80, twoThumbSlider.getExtent());
}

function testKeyHandlingLargeStepSize() {
  twoThumbSlider.setValue(0);
  twoThumbSlider.setExtent(100);
  twoThumbSlider.setStep(5);
  assertEquals(0, twoThumbSlider.getValue());
  assertEquals(100, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.RIGHT);
  assertEquals(5, twoThumbSlider.getValue());
  assertEquals(95, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.RIGHT);
  assertEquals(10, twoThumbSlider.getValue());
  assertEquals(90, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.LEFT);
  assertEquals(5, twoThumbSlider.getValue());
  assertEquals(90, twoThumbSlider.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSlider.getElement(), goog.events.KeyCodes.LEFT);
  assertEquals(0, twoThumbSlider.getValue());
  assertEquals(90, twoThumbSlider.getExtent());
}

function testKeyHandlingRtl() {
  twoThumbSliderRtl.setValue(0);
  twoThumbSliderRtl.setExtent(100);
  assertEquals(0, twoThumbSliderRtl.getValue());
  assertEquals(100, twoThumbSliderRtl.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSliderRtl.getElement(), goog.events.KeyCodes.RIGHT);
  assertEquals(0, twoThumbSliderRtl.getValue());
  assertEquals(99, twoThumbSliderRtl.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSliderRtl.getElement(), goog.events.KeyCodes.RIGHT);
  assertEquals(0, twoThumbSliderRtl.getValue());
  assertEquals(98, twoThumbSliderRtl.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSliderRtl.getElement(), goog.events.KeyCodes.LEFT);
  assertEquals(1, twoThumbSliderRtl.getValue());
  assertEquals(98, twoThumbSliderRtl.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSliderRtl.getElement(), goog.events.KeyCodes.LEFT);
  assertEquals(2, twoThumbSliderRtl.getValue());
  assertEquals(98, twoThumbSliderRtl.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSliderRtl.getElement(), goog.events.KeyCodes.RIGHT,
      {shiftKey: true});
  assertEquals(0, twoThumbSliderRtl.getValue());
  assertEquals(90, twoThumbSliderRtl.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSliderRtl.getElement(), goog.events.KeyCodes.RIGHT,
      {shiftKey: true});
  assertEquals(0, twoThumbSliderRtl.getValue());
  assertEquals(80, twoThumbSliderRtl.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSliderRtl.getElement(), goog.events.KeyCodes.LEFT,
      {shiftKey: true});
  assertEquals(10, twoThumbSliderRtl.getValue());
  assertEquals(80, twoThumbSliderRtl.getExtent());

  goog.testing.events.fireKeySequence(
      twoThumbSliderRtl.getElement(), goog.events.KeyCodes.LEFT,
      {shiftKey: true});
  assertEquals(20, twoThumbSliderRtl.getValue());
  assertEquals(80, twoThumbSliderRtl.getExtent());
}

function testRangeHighlight() {
  var rangeHighlight = goog.dom.getElement('rangeHighlight');

  // Test [0, 100]
  twoThumbSlider.setValue(0);
  twoThumbSlider.setExtent(100);
  assertHighlightedRange(rangeHighlight, 0, 100);

  // Test [25, 75]
  twoThumbSlider.setValue(25);
  twoThumbSlider.setExtent(50);
  assertHighlightedRange(rangeHighlight, 25, 75);

  // Test [50, 50]
  twoThumbSlider.setValue(50);
  twoThumbSlider.setExtent(0);
  assertHighlightedRange(rangeHighlight, 50, 50);
}

function testRangeHighlightAnimation() {
  var animationDelay = 160;  // Delay in ms, is a bit higher than actual delay.
  if (goog.userAgent.IE) {
    // For some reason, (probably due to how timing works), IE7 and IE8 will not
    // stop if we don't wait for it.
    animationDelay = 250;
  }

  var rangeHighlight = goog.dom.getElement('rangeHighlight');
  twoThumbSlider.setValue(0);
  twoThumbSlider.setExtent(100);

  // Animate right thumb, final range is [0, 75]
  twoThumbSlider.animatedSetValue(75);
  assertHighlightedRange(rangeHighlight, 0, 100);
  mockClock.tick(animationDelay);
  assertHighlightedRange(rangeHighlight, 0, 75);

  // Animate left thumb, final range is [25, 75]
  twoThumbSlider.animatedSetValue(25);
  assertHighlightedRange(rangeHighlight, 0, 75);
  mockClock.tick(animationDelay);
  assertHighlightedRange(rangeHighlight, 25, 75);
}


/**
 * Verifies that no error occurs and that the range highlight is sized correctly
 * for a zero-size slider (i.e. doesn't attempt to set a negative size). The
 * test tries to resize the slider from its original size to 0, then checks
 * that the range highlight's size is correctly set to 0.
 *
 * The size verification is needed because Webkit/Gecko outright ignore calls
 * to set negative sizes on an element, leaving it at its former size. IE
 * throws an error in the same situation.
 */
function testRangeHighlightForZeroSizeSlider() {
  // Make sure range highlight spans whole slider before zeroing width.
  twoThumbSlider.setExtent(100);
  twoThumbSlider.getElement().style.width = 0;

  // The setVisible call is used to force a UI update.
  twoThumbSlider.setVisible(true);
  assertEquals(
      'Range highlight size should be 0 when slider size is 0', 0,
      goog.dom.getElement('rangeHighlight').offsetWidth);
}

function testAnimatedSetValueAnimatesFactoryCreatedAnimations() {
  // Create and set the factory.
  var ignore = goog.testing.mockmatchers.ignoreArgument;
  var mockControl = new goog.testing.MockControl();
  var mockAnimation1 = mockControl.createLooseMock(goog.fx.Animation);
  var mockAnimation2 = mockControl.createLooseMock(goog.fx.Animation);
  var testAnimations = [mockAnimation1, mockAnimation2];
  oneThumbSlider.setAdditionalAnimations(new AnimationFactory(testAnimations));

  // Expect the animations to be played.
  mockAnimation1.play(false);
  mockAnimation2.play(false);
  mockAnimation1.addEventListener(ignore, ignore, ignore);
  mockAnimation2.addEventListener(ignore, ignore, ignore);

  // Animate and verify.
  mockControl.$replayAll();
  oneThumbSlider.animatedSetValue(50);
  mockControl.$verifyAll();
  mockControl.$resetAll();
  mockControl.$tearDown();
}

function testMouseWheelEventHandlerEnable() {
  // Mouse wheel handling should be enabled by default.
  assertTrue(oneThumbSlider.isHandleMouseWheel());

  // Test disabling the mouse wheel handler
  oneThumbSlider.setHandleMouseWheel(false);
  assertFalse(oneThumbSlider.isHandleMouseWheel());

  // Test that enabling again works fine.
  oneThumbSlider.setHandleMouseWheel(true);
  assertTrue(oneThumbSlider.isHandleMouseWheel());

  // Test that mouse wheel handling can be disabled before rendering a slider.
  var wheelDisabledElem = goog.dom.createDom(
      goog.dom.TagName.DIV, {}, goog.dom.createDom(goog.dom.TagName.SPAN));
  var wheelDisabledSlider = new OneThumbSlider();
  wheelDisabledSlider.setHandleMouseWheel(false);
  wheelDisabledSlider.decorate(wheelDisabledElem);
  assertFalse(wheelDisabledSlider.isHandleMouseWheel());
}

function testDisabledAndEnabledSlider() {
  // Check that a slider is enabled by default
  assertTrue(oneThumbSlider.isEnabled());

  var listenerCount = oneThumbSlider.getHandler().getListenerCount();
  // Disable the slider and check its state
  oneThumbSlider.setEnabled(false);
  assertFalse(oneThumbSlider.isEnabled());
  assertTrue(
      goog.dom.classlist.contains(
          oneThumbSlider.getElement(), 'goog-slider-disabled'));
  assertEquals(0, oneThumbSlider.getHandler().getListenerCount());

  // setValue should work unaffected even when the slider is disabled.
  oneThumbSlider.setValue(30);
  assertEquals(30, oneThumbSlider.getValue());
  assertEquals(
      'Setting valid value must dispatch a change event ' +
          'even when slider is disabled.',
      1, oneChangeEventCount);

  // Test the transition from disabled to enabled
  oneThumbSlider.setEnabled(true);
  assertTrue(oneThumbSlider.isEnabled());
  assertFalse(
      goog.dom.classlist.contains(
          oneThumbSlider.getElement(), 'goog-slider-disabled'));
  assertTrue(listenerCount == oneThumbSlider.getHandler().getListenerCount());
}

function testBlockIncrementingWithEnableAndDisabled() {
  var doc = goog.dom.getOwnerDocument(oneThumbSlider.getElement());
  // Case when slider is not disabled between the mouse down and up events.
  goog.testing.events.fireMouseDownEvent(oneThumbSlider.getElement());
  assertEquals(
      1, goog.events
             .getListeners(
                 oneThumbSlider.getElement(), goog.events.EventType.MOUSEMOVE,
                 false)
             .length);
  assertEquals(
      1, goog.events.getListeners(doc, goog.events.EventType.MOUSEUP, true)
             .length);

  goog.testing.events.fireMouseUpEvent(oneThumbSlider.getElement());

  assertEquals(
      0, goog.events
             .getListeners(
                 oneThumbSlider.getElement(), goog.events.EventType.MOUSEMOVE,
                 false)
             .length);
  assertEquals(
      0, goog.events.getListeners(doc, goog.events.EventType.MOUSEUP, true)
             .length);

  // Case when the slider is disabled between the mouse down and up events.
  goog.testing.events.fireMouseDownEvent(oneThumbSlider.getElement());
  assertEquals(
      1, goog.events
             .getListeners(
                 oneThumbSlider.getElement(), goog.events.EventType.MOUSEMOVE,
                 false)
             .length);
  assertEquals(
      1, goog.events.getListeners(doc, goog.events.EventType.MOUSEUP, true)
             .length);

  oneThumbSlider.setEnabled(false);

  assertEquals(
      0, goog.events
             .getListeners(
                 oneThumbSlider.getElement(), goog.events.EventType.MOUSEMOVE,
                 false)
             .length);
  assertEquals(
      0, goog.events.getListeners(doc, goog.events.EventType.MOUSEUP, true)
             .length);
  assertEquals(1, oneThumbSlider.getHandler().getListenerCount());

  goog.testing.events.fireMouseUpEvent(oneThumbSlider.getElement());
  assertEquals(
      0, goog.events
             .getListeners(
                 oneThumbSlider.getElement(), goog.events.EventType.MOUSEMOVE,
                 false)
             .length);
  assertEquals(
      0, goog.events.getListeners(doc, goog.events.EventType.MOUSEUP, true)
             .length);
}

function testMouseClickWithMoveToPointEnabled() {
  var stepSize = 20;
  oneThumbSlider.setStep(stepSize);
  oneThumbSlider.setMoveToPointEnabled(true);
  var initialValue = oneThumbSlider.getValue();

  // Figure out the number of pixels per step.
  var numSteps = Math.round(
      (oneThumbSlider.getMaximum() - oneThumbSlider.getMinimum()) / stepSize);
  var size = goog.style.getSize(oneThumbSlider.getElement());
  var pixelsPerStep = Math.round(size.width / numSteps);

  var coords = goog.style.getClientPosition(oneThumbSlider.getElement());
  coords.x += pixelsPerStep / 2;

  // Case when value is increased
  goog.testing.events.fireClickSequence(
      oneThumbSlider.getElement(),
      /* opt_button */ undefined, coords);
  assertEquals(oneThumbSlider.getValue(), initialValue + stepSize);

  // Case when value is decreased
  goog.testing.events.fireClickSequence(
      oneThumbSlider.getElement(),
      /* opt_button */ undefined, coords);
  assertEquals(oneThumbSlider.getValue(), initialValue);

  // Case when thumb is clicked
  goog.testing.events.fireClickSequence(oneThumbSlider.getElement());
  assertEquals(oneThumbSlider.getValue(), initialValue);
}

function testNonIntegerStepSize() {
  var stepSize = 0.02;
  oneThumbSlider.setStep(stepSize);
  oneThumbSlider.setMinimum(-1);
  oneThumbSlider.setMaximum(1);
  oneThumbSlider.setValue(0.7);
  assertRoughlyEquals(0.7, oneThumbSlider.getValue(), 0.000001);
  oneThumbSlider.setValue(0.3);
  assertRoughlyEquals(0.3, oneThumbSlider.getValue(), 0.000001);
}

function testSingleThumbSliderHasZeroExtent() {
  var stepSize = 0.02;
  oneThumbSlider.setStep(stepSize);
  oneThumbSlider.setMinimum(-1);
  oneThumbSlider.setMaximum(1);
  oneThumbSlider.setValue(0.7);
  assertEquals(0, oneThumbSlider.getExtent());
  oneThumbSlider.setValue(0.3);
  assertEquals(0, oneThumbSlider.getExtent());
}


/**
 * Tests getThumbCoordinateForValue method.
 */
function testThumbCoordinateForValueWithHorizontalSlider() {
  // Make sure the y-coordinate stays the same for the horizontal slider.
  var originalY = goog.style.getPosition(oneThumbSlider.valueThumb).y;
  var width = oneThumbSlider.getElement().clientWidth -
      oneThumbSlider.valueThumb.offsetWidth;
  var range = oneThumbSlider.getMaximum() - oneThumbSlider.getMinimum();

  // Verify coordinate for a particular value.
  var value = 20;
  var expectedX = Math.round(value / range * width);
  var expectedCoord = new goog.math.Coordinate(expectedX, originalY);
  var coord = oneThumbSlider.getThumbCoordinateForValue(value);
  assertObjectEquals(expectedCoord, coord);

  // Verify this works regardless of current position.
  oneThumbSlider.setValue(value / 2);
  coord = oneThumbSlider.getThumbCoordinateForValue(value);
  assertObjectEquals(expectedCoord, coord);
}

function testThumbCoordinateForValueWithVerticalSlider() {
  // Make sure the x-coordinate stays the same for the vertical slider.
  oneThumbSlider.setOrientation(goog.ui.SliderBase.Orientation.VERTICAL);
  var originalX = goog.style.getPosition(oneThumbSlider.valueThumb).x;
  var height = oneThumbSlider.getElement().clientHeight -
      oneThumbSlider.valueThumb.offsetHeight;
  var range = oneThumbSlider.getMaximum() - oneThumbSlider.getMinimum();

  // Verify coordinate for a particular value.
  var value = 20;
  var expectedY = height - Math.round(value / range * height);
  var expectedCoord = new goog.math.Coordinate(originalX, expectedY);
  var coord = oneThumbSlider.getThumbCoordinateForValue(value);
  assertObjectEquals(expectedCoord, coord);

  // Verify this works regardless of current position.
  oneThumbSlider.setValue(value / 2);
  coord = oneThumbSlider.getThumbCoordinateForValue(value);
  assertObjectEquals(expectedCoord, coord);
}


/**
 * Tests getValueFromMousePosition method.
 */
function testValueFromMousePosition() {
  var value = 30;
  oneThumbSlider.setValue(value);
  var offset = goog.style.getPageOffset(oneThumbSlider.valueThumb);
  var size = goog.style.getSize(oneThumbSlider.valueThumb);
  offset.x += size.width / 2;
  offset.y += size.height / 2;
  var e = null;
  goog.events.listen(
      oneThumbSlider, goog.events.EventType.MOUSEMOVE,
      function(evt) { e = evt; });
  goog.testing.events.fireMouseMoveEvent(oneThumbSlider, offset);
  assertNotEquals(e, null);
  assertRoughlyEquals(
      value, Math.round(oneThumbSlider.getValueFromMousePosition(e)), 1);
  // Verify this works regardless of current position.
  oneThumbSlider.setValue(value / 2);
  assertRoughlyEquals(
      value, Math.round(oneThumbSlider.getValueFromMousePosition(e)), 1);
}


/**
 * Tests ignoring click event after mousedown event.
 */
function testClickAfterMousedown() {
  // Get the center of the thumb at value zero.
  oneThumbSlider.setValue(0);
  var offset = goog.style.getPageOffset(oneThumbSlider.valueThumb);
  var size = goog.style.getSize(oneThumbSlider.valueThumb);
  offset.x += size.width / 2;
  offset.y += size.height / 2;

  var sliderElement = oneThumbSlider.getElement();
  var width = sliderElement.clientWidth - size.width;
  var range = oneThumbSlider.getMaximum() - oneThumbSlider.getMinimum();
  var offsetXAtZero = offset.x;

  // Temporarily control time.
  var theTime = goog.now();
  var saveGoogNow = goog.now;
  goog.now = function() { return theTime; };

  // set coordinate for a particular value.
  var valueOne = 10;
  offset.x = offsetXAtZero + Math.round(valueOne / range * width);
  goog.testing.events.fireMouseDownEvent(sliderElement, null, offset);
  assertEquals(valueOne, oneThumbSlider.getValue());

  // Verify a click event with another value that follows quickly is ignored.
  theTime += oneThumbSlider.MOUSE_DOWN_DELAY_ / 2;
  var valueTwo = 20;
  offset.x = offsetXAtZero + Math.round(valueTwo / range * width);
  goog.testing.events.fireClickEvent(sliderElement, null, offset);
  assertEquals(valueOne, oneThumbSlider.getValue());

  // Verify a click later in time does move the thumb.
  theTime += oneThumbSlider.MOUSE_DOWN_DELAY_;
  goog.testing.events.fireClickEvent(sliderElement, null, offset);
  assertEquals(valueTwo, oneThumbSlider.getValue());

  goog.now = saveGoogNow;
}


/**
 * Tests dragging events.
 */
function testDragEvents() {
  var offset = goog.style.getPageOffset(oneThumbSlider.valueThumb);
  var size = goog.style.getSize(oneThumbSlider.valueThumb);
  offset.x += size.width / 2;
  offset.y += size.height / 2;
  var event_types = [];
  var handler = function(evt) {
    event_types.push(evt.type);
  };

  goog.events.listen(
      oneThumbSlider,
      [
        goog.ui.SliderBase.EventType.DRAG_START,
        goog.ui.SliderBase.EventType.DRAG_END,
        goog.ui.SliderBase.EventType.DRAG_VALUE_START,
        goog.ui.SliderBase.EventType.DRAG_VALUE_END,
        goog.ui.SliderBase.EventType.DRAG_EXTENT_START,
        goog.ui.SliderBase.EventType.DRAG_EXTENT_END,
        goog.ui.Component.EventType.CHANGE
      ],
      handler);

  // Since the order of the events between value and extent is not guaranteed
  // across browsers, we need to allow for both here and once we have
  // them all, make sure that they were different.
  function isValueOrExtentDragStart(type) {
    return type == goog.ui.SliderBase.EventType.DRAG_VALUE_START ||
        type == goog.ui.SliderBase.EventType.DRAG_EXTENT_START;
  }
  function isValueOrExtentDragEnd(type) {
    return type == goog.ui.SliderBase.EventType.DRAG_VALUE_END ||
        type == goog.ui.SliderBase.EventType.DRAG_EXTENT_END;
  }

  // Test that dragging the thumb calls all the correct events.
  goog.testing.events.fireMouseDownEvent(oneThumbSlider.valueThumb);
  offset.x += 100;
  goog.testing.events.fireMouseMoveEvent(oneThumbSlider.valueThumb, offset);
  goog.testing.events.fireMouseUpEvent(oneThumbSlider.valueThumb);

  assertEquals(9, event_types.length);

  assertEquals(goog.ui.SliderBase.EventType.DRAG_START, event_types[0]);
  assertTrue(isValueOrExtentDragStart(event_types[1]));

  assertEquals(goog.ui.SliderBase.EventType.DRAG_START, event_types[2]);
  assertTrue(isValueOrExtentDragStart(event_types[3]));

  assertEquals(goog.ui.Component.EventType.CHANGE, event_types[4]);

  assertEquals(goog.ui.SliderBase.EventType.DRAG_END, event_types[5]);
  assertTrue(isValueOrExtentDragEnd(event_types[6]));

  assertEquals(goog.ui.SliderBase.EventType.DRAG_END, event_types[7]);
  assertTrue(isValueOrExtentDragEnd(event_types[8]));

  assertFalse(event_types[1] == event_types[3]);
  assertFalse(event_types[6] == event_types[8]);

  // Test that clicking the thumb without moving the mouse does not cause a
  // CHANGE event between DRAG_START/DRAG_END.
  event_types = [];
  goog.testing.events.fireMouseDownEvent(oneThumbSlider.valueThumb);
  goog.testing.events.fireMouseUpEvent(oneThumbSlider.valueThumb);

  assertEquals(8, event_types.length);

  assertEquals(goog.ui.SliderBase.EventType.DRAG_START, event_types[0]);
  assertTrue(isValueOrExtentDragStart(event_types[1]));

  assertEquals(goog.ui.SliderBase.EventType.DRAG_START, event_types[2]);
  assertTrue(isValueOrExtentDragStart(event_types[3]));

  assertEquals(goog.ui.SliderBase.EventType.DRAG_END, event_types[4]);
  assertTrue(isValueOrExtentDragEnd(event_types[5]));

  assertEquals(goog.ui.SliderBase.EventType.DRAG_END, event_types[6]);
  assertTrue(isValueOrExtentDragEnd(event_types[7]));

  assertFalse(event_types[1] == event_types[3]);
  assertFalse(event_types[5] == event_types[7]);

  // Early listener removal, do not wait for tearDown, to avoid building up
  // arrays of events unnecessarilly in further tests.
  goog.events.removeAll(oneThumbSlider);
}


/**
 * Tests dragging events updates the value correctly in LTR mode based on the
 * amount of space remaining to the right of the thumb.
 */
function testDragEventsUpdatesValue() {
  // Get the center of the thumb at minimum value.
  oneThumbSlider.setMinimum(100);
  oneThumbSlider.setMaximum(300);
  oneThumbSlider.setValue(100);

  // Need to set to (0, 0) in IE8 due to a browser bug where the
  // offsetWidth/height is incorrectly calculated as 0 in test files.
  var offset = (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9')) ?
      new goog.math.Coordinate() :
      goog.style.getPageOffset(oneThumbSlider.valueThumb);
  var offsetXAtZero = offset.x;

  var sliderElement = oneThumbSlider.getElementStrict();
  var thumbSize = goog.style.getSize(oneThumbSlider.valueThumb);
  var width = sliderElement.clientWidth - thumbSize.width;
  var range = oneThumbSlider.getMaximum() - oneThumbSlider.getMinimum();

  // Test that dragging the thumb calls all the correct events.
  goog.testing.events.fireMouseDownEvent(oneThumbSlider.valueThumb);

  // Scroll to 30 in the range of 0-200. Given that this is LTR mode, that means
  // the value will be 100 + 30 = 130.
  offset.x = offsetXAtZero + Math.round(30 / range * width);
  goog.testing.events.fireMouseMoveEvent(oneThumbSlider.valueThumb, offset);
  assertEquals(130, oneThumbSlider.getValue());

  // Scroll to 70 in the range of 0-200. Given that this is LTR mode, that means
  // the value will be 100 + 70 = 170.
  offset.x = offsetXAtZero + Math.round(70 / range * width);
  goog.testing.events.fireMouseMoveEvent(oneThumbSlider.valueThumb, offset);
  assertEquals(170, oneThumbSlider.getValue());


  goog.testing.events.fireMouseUpEvent(oneThumbSlider.valueThumb);
}


/**
 * Tests dragging events updates the value correctly in RTL mode based on the
 * amount of space remaining to the left of the thumb.
 */
function testDragEventsInRtlModeUpdatesValue() {
  // Get the center of the thumb at minimum value.
  oneThumbSliderRtl.setMinimum(100);
  oneThumbSliderRtl.setMaximum(300);
  oneThumbSliderRtl.setValue(100);

  var offset = goog.style.getPageOffset(oneThumbSliderRtl.valueThumb);
  var offsetXAtZero = offset.x;
  // Extra half of the thumb width in IE8 due to a browser bug where the thumb
  // offsetWidth is incorrectly calculated as 0 in test files.
  var thumbSize = goog.style.getSize(oneThumbSliderRtl.valueThumb);
  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9')) {
    offsetXAtZero += thumbSize.width / 2;
  }

  var sliderElement = oneThumbSliderRtl.getElementStrict();
  var width = sliderElement.clientWidth - thumbSize.width;
  var range = oneThumbSliderRtl.getMaximum() - oneThumbSliderRtl.getMinimum();

  // Test that dragging the thumb calls all the correct events.
  goog.testing.events.fireMouseDownEvent(oneThumbSliderRtl.valueThumb);

  // Scroll to 30 in the range of 0-200. Given that this is RTL mode, that means
  // the value will be 100 - (-30) = 130.
  offset.x = offsetXAtZero - Math.round(30 / range * width);
  goog.testing.events.fireMouseMoveEvent(oneThumbSliderRtl.valueThumb, offset);
  assertEquals(130, oneThumbSliderRtl.getValue());

  // Scroll to 70 in the range of 0-200. Given that this is RTL mode, that means
  // the value will be 100 - (-70) = 170.
  offset.x = offsetXAtZero - Math.round(70 / range * width);
  goog.testing.events.fireMouseMoveEvent(oneThumbSliderRtl.valueThumb, offset);
  assertEquals(170, oneThumbSliderRtl.getValue());

  goog.testing.events.fireMouseUpEvent(oneThumbSliderRtl.valueThumb);
}


/**
 * Tests animationend event after click.
 */
function testAnimationEndEventAfterClick() {
  var offset = goog.style.getPageOffset(oneThumbSlider.valueThumb);
  var size = goog.style.getSize(oneThumbSlider.valueThumb);
  offset.x += size.width / 2;
  offset.y += size.height / 2;
  var event_types = [];
  var handler = function(evt) {
    event_types.push(evt.type);
  };
  var animationDelay = 160;  // Delay in ms, is a bit higher than actual delay.
  if (goog.userAgent.IE) {
    // For some reason, (probably due to how timing works), IE7 and IE8 will not
    // stop if we don't wait for it.
    animationDelay = 250;
  }
  oneThumbSlider.setMoveToPointEnabled(true);
  goog.events.listen(
      oneThumbSlider, goog.ui.SliderBase.EventType.ANIMATION_END, handler);

  function isAnimationEndType(type) {
    return type == goog.ui.SliderBase.EventType.ANIMATION_END;
  }
  offset.x += 100;
  goog.testing.events.fireClickSequence(
      oneThumbSlider.getElement(), /* opt_button */ undefined, offset);

  mockClock.tick(animationDelay);
  assertEquals(1, event_types.length);
  assertTrue(isAnimationEndType(event_types[0]));
  goog.events.removeAll(oneThumbSlider);
}

/**
 * Tests that focus will be on the top level element when clicking the slider if
 * `focusElementOnSliderDrag` is true.
 */
function testFocusOnSliderAfterClickIfFocusElementOnSliderDragTrue() {
  var sliderElement = oneThumbSlider.getElement();
  var coords = goog.style.getClientPosition(sliderElement);
  goog.testing.events.fireClickSequence(
      sliderElement, /* opt_button */ undefined, coords);

  var activeElement = oneThumbSlider.getDomHelper().getActiveElement();
  assertEquals(sliderElement, activeElement);
}

/**
 * Tests that focus will not be on the top level element when clicking the
 * slider if `focusElementOnSliderDrag` is false.
 */
function testFocusNotOnSliderAfterClickIfFocusElementOnSliderDragFalse() {
  oneThumbSlider.setFocusElementOnSliderDrag(false);
  var sliderElement = oneThumbSlider.getElement();
  var coords = goog.style.getClientPosition(sliderElement);
  goog.testing.events.fireClickSequence(
      sliderElement, /* opt_button */ undefined, coords);

  var activeElement = oneThumbSlider.getDomHelper().getActiveElement();
  assertNotEquals(sliderElement, activeElement);
}
