text: /*
   * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
   *  
   * Permission is hereby granted, free of charge, to any person obtaining a
   * copy of this software and associated documentation files (the "Software"), 
   * to deal in the Software without restriction, including without limitation 
   * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
   * and/or sell copies of the Software, and to permit persons to whom the 
   * Software is furnished to do so, subject to the following conditions:
   *  
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *  
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
   * DEALINGS IN THE SOFTWARE.
   * 
   */
  
  
  /*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
  /*global define, describe, it, expect, beforeEach, afterEach, waits, waitsFor, runs, $, brackets, waitsForDone, spyOn, tinycolor, KeyEvent */
  
  define(function (require, exports, module) {
      "use strict";
  
      // Modules from the SpecRunner window
      var SpecRunnerUtils   = brackets.getModule("spec/SpecRunnerUtils"),
          Editor            = brackets.getModule("editor/Editor").Editor,
          DocumentManager   = brackets.getModule("document/DocumentManager"),
          Strings           = brackets.getModule("strings"),
          KeyEvent          = brackets.getModule("utils/KeyEvent"),
          testContentCSS    = require("text!unittest-files/unittests.css"),
          testContentHTML   = require("text!unittest-files/unittests.html"),
          provider          = require("main").inlineColorEditorProvider,
          InlineColorEditor = require("InlineColorEditor").InlineColorEditor,
          ColorEditor       = require("ColorEditor").ColorEditor;
  
      require("thirdparty/tinycolor-min");
  
      describe("Inline Color Editor - unit", function () {
  
          var testDocument, testEditor, inline;
          
          /**
           * Creates an inline color editor connected to the given cursor position in the test editor.
           * Note that this does *not* actually open it as an inline editor in the test editor.
           * Tests that use this must wrap their contents in a runs() block.
           * @param {!{line:number, ch: number}} cursor Position for which to open the inline editor.
           * if the provider did not create an inline editor.
           */
          function makeColorEditor(cursor) {
              runs(function () {
                  var promise = provider(testEditor, cursor);
                  if (promise) {
                      promise.done(function (inlineResult) {
                          inlineResult.onAdded();
                          inline = inlineResult;
                      });
                      waitsForDone(promise, "open color editor");
                  }
              });
          }
              
          /**
           * Expects an inline editor to be opened at the given cursor position and to have the
           * given initial color (which should match the color at that position).
           * @param {!{line:number, ch:number}} cursor The cursor position to try opening the inline at.
           * @param {string} color The expected color.
           */
          function testOpenColor(cursor, color) {
              makeColorEditor(cursor);
              runs(function () {
                  expect(inline).toBeTruthy();
                  expect(inline._color).toBe(color);
              });
          }
          
          /**
           * Simulate the given event with clientX/clientY specified by the given
           * ratios of the item's actual width/height (offset by the left/top of the
           * item).
           * @param {string} event The name of the event to simulate.
           * @param {object} $item A jQuery object to trigger the event on.
           * @param {Array.<number>} ratios Numbers between 0 and 1 indicating the x and y positions of the
           *      event relative to the item's width and height.
           */
          function eventAtRatio(event, $item, ratios) {
              $item.trigger($.Event(event, {
                  clientX: $item.offset().left + (ratios[0] * $item.width()),
                  clientY: $item.offset().top + (ratios[1] * $item.height())
              }));
          }
                  
          describe("Inline editor - CSS", function () {
  
              beforeEach(function () {
                  var mock = SpecRunnerUtils.createMockEditor(testContentCSS, "css");
                  testDocument = mock.doc;
                  testEditor = mock.editor;
              });
              
              afterEach(function () {
                  SpecRunnerUtils.destroyMockEditor(testDocument);
                  testEditor = null;
                  testDocument = null;
                  inline = null;
              });
           
              describe("simple open cases", function () {
                  
                  it("should show the correct color when opened on an #rrggbb color", function () {
                      testOpenColor({line: 1, ch: 18}, "#abcdef");
                  });
                  it("should open when at the beginning of the color", function () {
                      testOpenColor({line: 1, ch: 16}, "#abcdef");
                  });
                  it("should open when at the end of the color", function () {
                      testOpenColor({line: 1, ch: 23}, "#abcdef");
                  });
                  it("should show the correct color when opened on an #rgb color", function () {
                      testOpenColor({line: 5, ch: 18}, "#abc");
                  });
                  it("should show the correct color when opened on an rgb() color", function () {
                      testOpenColor({line: 9, ch: 18}, "rgb(100, 200, 150)");
                  });
                  it("should show the correct color when opened on an rgba() color", function () {
                      testOpenColor({line: 13, ch: 18}, "rgba(100, 200, 150, 0.5)");
                  });
                  it("should show the correct color when opened on an hsl() color", function () {
                      testOpenColor({line: 17, ch: 18}, "hsl(180, 50%, 50%)");
                  });
                  it("should show the correct color when opened on an hsla() color", function () {
                      testOpenColor({line: 21, ch: 18}, "hsla(180, 50%, 50%, 0.5)");
                  });
                  it("should show the correct color when opened on an uppercase hex color", function () {
                      testOpenColor({line: 33, ch: 18}, "#DEFCBA");
                  });
                  it("should show the correct color when opened on a color in a shorthand property", function () {
                      testOpenColor({line: 41, ch: 27}, "#0f0f0f");
                  });
                  it("should show the correct color when opened on an rgba() color with a leading period in the alpha field", function () {
                      testOpenColor({line: 45, ch: 18}, "rgba(100, 200, 150, .5)");
                  });
                  it("should show the correct color when opened on an hsla() color with a leading period in the alpha field", function () {
                      testOpenColor({line: 49, ch: 18}, "hsla(180, 50%, 50%, .5)");
                  });
                  
                  it("should not open when not on a color", function () {
                      makeColorEditor({line: 1, ch: 6});
                      runs(function () {
                          expect(inline).toEqual(null);
                      });
                  });
                  it("should not open when on an invalid color", function () {
                      makeColorEditor({line: 25, ch: 18});
                      runs(function () {
                          expect(inline).toEqual(null);
                      });
                  });
                  it("should not open when on an hsl color with missing percent signs", function () {
                      makeColorEditor({line: 37, ch: 18});
                      runs(function (inline) {
                          expect(inline).toEqual(null);
                      });
                  });
                  
                  it("should open on the second color when there are two colors in the same line", function () {
                      testOpenColor({line: 29, ch: 48}, "#ddeeff");
                  });
              
                  it("should properly add/remove ref to document when opened/closed", function () {
                      runs(function () {
                          spyOn(testDocument, "addRef").andCallThrough();
                          spyOn(testDocument, "releaseRef").andCallThrough();
                      });
                      makeColorEditor({line: 1, ch: 18});
                      runs(function () {
                          expect(testDocument.addRef).toHaveBeenCalled();
                          expect(testDocument.addRef.callCount).toBe(1);
                          
                          inline.onClosed();
                          expect(testDocument.releaseRef).toHaveBeenCalled();
                          expect(testDocument.releaseRef.callCount).toBe(1);
                      });
                  });
                  
              });
              
              describe("update host document on edit in color editor", function () {
                  
                  it("should update host document when change is committed in color editor", function () {
                      makeColorEditor({line: 1, ch: 18});
                      runs(function () {
                          inline.colorEditor.setColorFromString("#c0c0c0");
                          expect(testDocument.getRange({line: 1, ch: 16}, {line: 1, ch: 23})).toBe("#c0c0c0");
                      });
                  });
                  
                  it("should update correct range of host document with color format of different length", function () {
                      makeColorEditor({line: 1, ch: 18});
                      runs(function () {
                          inline.colorEditor.setColorFromString("rgb(20, 20, 20)");
                          expect(testDocument.getRange({line: 1, ch: 16}, {line: 1, ch: 31})).toBe("rgb(20, 20, 20)");
                      });
                  });
      
                  it("should not invalidate range when change is committed", function () {
                      makeColorEditor({line: 1, ch: 18});
                      runs(function () {
                          inline.colorEditor.setColorFromString("rgb(20, 20, 20)");
                          expect(inline.getCurrentRange()).not.toBeNull();
                      });
                  });
      
              });
              
              describe("update color editor on edit in host editor", function () {
                  
                  it("should update when edit is made to color range in host editor", function () {
                      makeColorEditor({line: 1, ch: 18});
                      runs(function () {
                          spyOn(inline, "close");
          
                          testDocument.replaceRange("0", {line: 1, ch: 18}, {line: 1, ch: 19});
                          expect(inline._color).toBe("#a0cdef");
                          // TODO (#2201): this assumes getColor() is a tinycolor, but sometimes it's a string
                          expect(inline.colorEditor.getColor().toHexString().toLowerCase()).toBe("#a0cdef");
                          expect(inline.close).not.toHaveBeenCalled();
                          expect(inline.getCurrentRange()).toEqual({start: {line: 1, ch: 16}, end: {line: 1, ch: 23}});
                      });
                  });
                  
                  it("should close itself if edit is made that destroys end bookmark and leaves color invalid", function () {
                      makeColorEditor({line: 1, ch: 18});
                      runs(function () {
                          spyOn(inline, "close");
                          
                          // Replace everything including the semicolon, so it crosses the bookmark boundary.
                          testDocument.replaceRange("rgb(255, 25", {line: 1, ch: 16}, {line: 1, ch: 24});
                          expect(inline.close).toHaveBeenCalled();
                      });
                  });
                  
                  it("should maintain the range if the user deletes the last character of the color and types a new one", function () {
                      makeColorEditor({line: 1, ch: 18});
                      runs(function () {
                          spyOn(inline, "close");
          
                          testDocument.replaceRange("", {line: 1, ch: 22}, {line: 1, ch: 23});
                          testDocument.replaceRange("0", {line: 1, ch: 22}, {line: 1, ch: 22});
                          expect(inline._color).toBe("#abcde0");
                          expect(inline.close).not.toHaveBeenCalled();
                          expect(inline.getCurrentRange()).toEqual({start: {line: 1, ch: 16}, end: {line: 1, ch: 23}});
                      });
                  });
                  
                  it("should not update the end bookmark to a shorter valid match if the bookmark still exists and the color becomes invalid", function () {
                      makeColorEditor({line: 1, ch: 18});
                      runs(function () {
                          testDocument.replaceRange("", {line: 1, ch: 22}, {line: 1, ch: 23});
                          expect(inline._color).toBe("#abcde");
                          expect(inline.getCurrentRange()).toEqual({start: {line: 1, ch: 16}, end: {line: 1, ch: 22}});
                      });
                  });
                  
                  // TODO: (issue #2166) The following test fails because if the end bookmark is deleted, we match the shorter
                  // #xxx string at the beginning of the color and assume that's valid, and then reset the bookmark
                  // to the end of that location.
      //            it("should not update the end bookmark to a shorter valid match if the bookmark no longer exists and the color becomes invalid", function () {
      //                makeColorEditor({line: 1, ch: 18}).done(function (inline) {
      //                    testDocument.replaceRange("", {line: 1, ch: 22}, {line: 1, ch: 24});
      //                    expect(inline._color).toBe("#abcde");
      //                    expect(inline.getCurrentRange()).toEqual({start: {line: 1, ch: 16}, end: {line: 1, ch: 22}});
      //                });
      //            });
                  
              });
              
              describe("edit batching", function () {
                  it("should combine multiple edits within the same inline editor into a single undo in the host editor", function () {
                      makeColorEditor({line: 1, ch: 18});
                      runs(function () {
                          inline.colorEditor.setColorFromString("#010101");
                          inline.colorEditor.setColorFromString("#123456");
                          inline.colorEditor.setColorFromString("#bdafe0");
                          testDocument._masterEditor._codeMirror.undo();
                          expect(testDocument.getRange({line: 1, ch: 16}, {line: 1, ch: 23})).toBe("#abcdef");
                      });
                  });
              });
          });
          
          describe("Inline editor - HTML", function () {
  
              beforeEach(function () {
                  var mock = SpecRunnerUtils.createMockEditor(testContentHTML, "html");
                  testDocument = mock.doc;
                  testEditor = mock.editor;
              });
              
              afterEach(function () {
                  SpecRunnerUtils.destroyMockEditor(testDocument);
                  testEditor = null;
                  testDocument = null;
              });
           
              it("should open on a color in an HTML file", function () {
                  testOpenColor({line: 4, ch: 30}, "#dead01");
              });
          });
              
          describe("Inline editor - used colors processing", function () {
              
              it("should trim the original array to the given length", function () {
                  var inline = new InlineColorEditor();
                  var result = inline._collateColors(["#abcdef", "#fedcba", "#aabbcc", "#bbccdd"], 2);
                  expect(result).toEqual([
                      {value: "#abcdef", count: 1},
                      {value: "#fedcba", count: 1}
                  ]);
              });
              
              it("should remove duplicates from the original array and sort it by usage", function () {
                  var inline = new InlineColorEditor();
                  var result = inline._collateColors(["#abcdef", "#fedcba", "#123456", "#FEDCBA", "#123456", "#123456", "rgb(100, 100, 100)"], 100);
                  expect(result).toEqual([
                      {value: "#123456", count: 3},
                      {value: "#fedcba", count: 2},
                      {value: "#abcdef", count: 1},
                      {value: "rgb(100, 100, 100)", count: 1}
                  ]);
              });
          });
          
          describe("Color editor UI", function () {
              var colorEditor,
                  defaultSwatches = [{value: "#abcdef", count: 3}, {value: "rgba(100, 200, 250, 0.5)", count: 2}];
              
              /**
               * Creates a hidden ColorEditor and appends it to the body. Note that this is a standalone
               * ColorEditor, not inside an InlineColorEditor.
               * @param {string} initialColor The color that should be initially set in the ColorEditor.
               * @param {?function} callback An optional callback to be passed as the ColorEditor's callback. If
               *     none is supplied, a dummy function is passed.
               * @param {?Array.<{value:string, count:number}>} swatches An optional array of swatches to display.
               *     If none is supplied, a default set of two swatches is passed.
               * @param {boolean=} hide Whether to hide the color picker; default is true.
               */
              function makeUI(initialColor, callback, swatches, hide) {
                  colorEditor = new ColorEditor($(document.body),
                                                initialColor,
                                                callback || function () { },
                                                swatches || defaultSwatches);
                  if (hide !== false) {
                      colorEditor.getRootElement().css("display", "none");
                  }
              }
                          
              afterEach(function () {
                  colorEditor.getRootElement().remove();
              });
              
              /**
               * Checks whether the difference between val1 and val2 is within the given tolerance.
               * (We can't use Jasmine's .toBeCloseTo() because that takes a precision in decimal places,
               * whereas we often need to check an absolute distance.)
               * @param {(number|string)} val1 The first value to check.
               * @param {(number|string)} val2 The second value to check.
               * @param {number} tolerance The desired tolerance.
               */
              function checkNear(val1, val2, tolerance) {
                  expect(Math.abs(Number(val1) - Number(val2)) < (tolerance || 1.0)).toBe(true);
              }
              /**
               * Checks whether the given percentage string is near the given value.
               * @param {string} pct The percentage to check. Assumed to be a string ending in "%".
               * @param {number} val The value to check against. Assumed to be a percentage number, but not ending in "%".
               */
              function checkPercentageNear(pct, val) {
                  expect(checkNear(pct.substr(0, pct.length - 1), val));
              }
              
              /** Returns the colorEditor's current value as a string in its current format */
              function getColorString() {
                  return tinycolor(colorEditor.getColor()).toString();
              }
              
              describe("simple load/commit", function () {
              
                  it("should load the initial color correctly", function () {
                      var colorStr    = "rgba(77, 122, 31, 0.5)";
                      var colorStrRgb = "rgb(77, 122, 31)";
                      
                      runs(function () {
                          makeUI(colorStr);
                          expect(colorEditor.getColor().toString()).toBe(colorStr);
                          expect(colorEditor.$colorValue.val()).toBe(colorStr);
                          expect(tinycolor.equals(colorEditor.$currentColor.css("background-color"), colorStr)).toBe(true);
      
                          // Not sure why the tolerances need to be larger for these.
                          checkNear(tinycolor(colorEditor.$selection.css("background-color")).toHsv().h, 90, 2.0);
                          checkNear(tinycolor(colorEditor.$hueBase.css("background-color")).toHsv().h, 90, 2.0);
      
                          expect(tinycolor.equals(colorEditor.$selectionBase.css("background-color"), colorStrRgb)).toBe(true);
                      });
  
                      // Need to do these on a timeout since we can't seem to read back CSS positions synchronously.
                      waits(1);
                      
                      runs(function () {
                          checkPercentageNear(colorEditor.$hueSelector[0].style.bottom, 25);
                          checkPercentageNear(colorEditor.$opacitySelector[0].style.bottom, 50);
                          checkPercentageNear(colorEditor.$selectionBase[0].style.left, 74);
                          checkPercentageNear(colorEditor.$selectionBase[0].style.bottom, 47);
                      });
                  });
                  
                  it("should load a committed color correctly", function () {
                      var colorStr = "rgba(77, 122, 31, 0.5)";
                      var colorStrRgb = "rgb(77, 122, 31)";
                      
                      runs(function () {
                          makeUI("#0a0a0a");
                          colorEditor.setColorFromString(colorStr);
                          expect(colorEditor.getColor().toString()).toBe(colorStr);
                          expect(colorEditor.$colorValue.val()).toBe(colorStr);
                          expect(tinycolor.equals(colorEditor.$currentColor.css("background-color"), colorStr)).toBe(true);
                          checkNear(tinycolor(colorEditor.$selection.css("background-color")).toHsv().h, tinycolor(colorStr).toHsv().h);
                          checkNear(tinycolor(colorEditor.$hueBase.css("background-color")).toHsv().h, tinycolor(colorStr).toHsv().h);
                          expect(tinycolor.equals(colorEditor.$selectionBase.css("background-color"), colorStrRgb)).toBe(true);
                      });
  
                      // Need to do these on a timeout since we can't seem to read back CSS positions synchronously.
                      waits(1);
  
                      runs(function () {
                          checkPercentageNear(colorEditor.$hueSelector[0].style.bottom, 25);
                          checkPercentageNear(colorEditor.$opacitySelector[0].style.bottom, 50);
                          checkPercentageNear(colorEditor.$selectionBase[0].style.left, 74);
                          checkPercentageNear(colorEditor.$selectionBase[0].style.bottom, 47);
                      });
                  });
      
                  it("should call the callback when a new color is committed", function () {
                      var lastColor;
                      makeUI("rgba(100, 100, 100, 0.5)", function (color) {
                          lastColor = color;
                      });
                      colorEditor.setColorFromString("#a0a0a0");
                      expect(lastColor).toBe("#a0a0a0");
                  });
                  
              });
              
              describe("conversions", function () {
                  
                  /**
                   * Test whether converting the given color to the given mode results in the expected color.
                   * @param {string} initialColor The color to convert.
                   * @param {string} mode The mode to convert to: most be "rgba", "hsla", or "hex".
                   * @param {string} result The expected result of the conversion.
                   */
                  function testConvert(initialColor, mode, result) {
                      makeUI(initialColor);
                      var buttonMap = {
                          "rgba": "$rgbaButton",
                          "hsla": "$hslButton",
                          "hex": "$hexButton"
                      };
                      colorEditor[buttonMap[mode]].trigger("click");
                      expect(colorEditor.getColor().toString()).toBe(result);
                  }
                  
                  it("should convert a hex color to rgb when mode button clicked", function () {
                      testConvert("#112233", "rgba", "rgb(17, 34, 51)");
                  });
                  it("should convert a hex color to hsl when mode button clicked", function () {
                      testConvert("#112233", "hsla", "hsl(210, 50%, 13%)");
                  });
                  it("should convert an rgb color to hex when mode button clicked", function () {
                      testConvert("rgb(15, 160, 21)", "hex", "#0fa015");
                  });
                  it("should convert an rgba color to hex (dropping alpha) when mode button clicked", function () {
                      testConvert("rgba(15, 160, 21, 0.5)", "hex", "#0fa015");
                  });
                  it("should convert an rgb color to hsl when mode button clicked", function () {
                      testConvert("rgb(15, 160, 21)", "hsla", "hsl(122, 83%, 34%)");
                  });
                  it("should convert an rgba color to hsla when mode button clicked", function () {
                      testConvert("rgba(15, 160, 21, 0.3)", "hsla", "hsla(122, 83%, 34%, 0.3)");
                  });
                  it("should convert an hsl color to hex when mode button clicked", function () {
                      testConvert("hsl(152, 12%, 22%)", "hex", "#313f39");
                  });
                  it("should convert an hsla color to hex (dropping alpha) when mode button clicked", function () {
                      testConvert("hsla(152, 12%, 22%, 0.7)", "hex", "#313f39");
                  });
                  it("should convert an hsl color to rgb when mode button clicked", function () {
                      testConvert("hsl(152, 12%, 22%)", "rgba", "rgb(49, 63, 57)");
                  });
                  it("should convert an hsla color to rgba when mode button clicked", function () {
                      testConvert("hsla(152, 12%, 22%, 0.7)", "rgba", "rgba(49, 63, 57, 0.7)");
                  });
                  
              });
              
              describe("parameter editing with mouse", function () {
                  
                  /**
                   * Test a mouse down event on the given UI element.
                   * @param {object} opts The parameters to test:
                   *     item: The (string) name of the member of ColorEditor that references the element to test.
                   *     clickAt: An [x, y] array specifying the simulated x/y mouse position as a fraction of the
                   *          item's width/height. For example, [0.5, 0.5] would specify a click exactly in the
                   *          center of the element.
                   *     param: The (string) parameter whose value we're testing (h, s, v, or a).
                   *     expected: The expected value for the parameter.
                   *     tolerance: The tolerance in variation for the expected value.
                   */
                  function testMousedown(opts) {
                      makeUI("#0000ff");
                      eventAtRatio("mousedown", colorEditor[opts.item], opts.clickAt);
                      checkNear(tinycolor(colorEditor.getColor()).toHsv()[opts.param], opts.expected, opts.tolerance);
                      colorEditor[opts.item].trigger("mouseup");  // clean up drag state
                  }
  
                  /**
                   * Test a drag event on the given UI element.
                   * @param {object} opts The parameters to test:
                   *     item: The (string) name of the member of ColorEditor that references the element to test.
                   *     clickAt: An [x, y] array specifying the simulated x/y mouse position for the initial mouse down
                   *          as a fraction of the item's width/height. For example, [0.5, 0.5] would specify a click 
                   *          exactly in the center of the element.
                   *     dragTo: An [x, y] array specifying the location to drag to, using the same convention as clickAt.
                   *     param: The (string) parameter whose value we're testing (h, s, v, or a).
                   *     expected: The expected value for the parameter.
                   *     tolerance: The tolerance in variation for the expected value.
                   */
                  function testDrag(opts) {
                      makeUI("#0000ff");
                      eventAtRatio("mousedown", colorEditor[opts.item], opts.clickAt);
                      eventAtRatio("mousemove", colorEditor[opts.item], opts.dragTo);
                      checkNear(tinycolor(colorEditor.getColor()).toHsv()[opts.param], opts.expected, opts.tolerance);
                      colorEditor[opts.item].trigger("mouseup");  // clean up drag state
                  }
                  
                  it("should set saturation on mousedown", function () {
                      testMousedown({
                          item:      "$selection",
                          clickAt:   [0.25, 0], // x: saturation, y: 1.0 - value
                          param:     "s",
                          expected:  0.25,
                          tolerance: 0.1
                      });
                  });
                  it("should set saturation on drag", function () {
                      testDrag({
                          item:      "$selection",
                          clickAt:   [0.25, 0], // x: saturation, y: 1.0 - value
                          dragTo:    [0.75, 0],
                          param:     "s",
                          expected:  0.75,
                          tolerance: 0.1
                      });
                  });
                  it("should clip saturation to min value", function () {
                      testDrag({
                          item:      "$selection",
                          clickAt:   [0.25, 0], // x: saturation, y: 1.0 - value
                          dragTo:    [-0.25, 0],
                          param:     "s",
                          expected:  0,
                          tolerance: 0.1
                      });
                  });
                  it("should clip saturation to max value", function () {
                      testDrag({
                          item:      "$selection",
                          clickAt:   [0.25, 0], // x: saturation, y: 1.0 - value
                          dragTo:    [1.25, 0],
                          param:     "s",
                          expected:  1,
                          tolerance: 0.1
                      });
                  });
                  it("should set value on mousedown", function () {
                      testMousedown({
                          item:      "$selection",
                          clickAt:   [1.0, 0.75], // x: saturation, y: 1.0 - value
                          param:     "v",
                          expected:  0.25,
                          tolerance: 0.1
                      });
                  });
                  it("should set value on drag", function () {
                      testDrag({
                          item:      "$selection",
                          clickAt:   [1.0, 0.75], // x: saturation, y: 1.0 - value
                          dragTo:    [1.0, 0.25],
                          param:     "v",
                          expected:  0.75,
                          tolerance: 0.1
                      });
                  });
                  it("should clip value to min value", function () {
                      testDrag({
                          item:      "$selection",
                          clickAt:   [1.0, 0.75], // x: saturation, y: 1.0 - value
                          dragTo:    [1.0, 1.25],
                          param:     "v",
                          expected:  0,
                          tolerance: 0.1
                      });
                  });
                  it("should clip value to max value", function () {
                      testDrag({
                          item:      "$selection",
                          clickAt:   [1.0, 0.75],
                          dragTo:    [1.0, -0.25],
                          param:     "v",
                          expected:  1,
                          tolerance: 0.1
                      });
                  });
                  it("should set hue on mousedown", function () {
                      testMousedown({
                          item:      "$hueSlider",
                          clickAt:   [0, 0.75], // x: unused, y: 1.0 - (hue / 360)
                          param:     "h",
                          expected:  90,
                          tolerance: 1
                      });
                  });
                  it("should set hue on drag", function () {
                      testDrag({
                          item:      "$hueSlider",
                          clickAt:   [0, 0.75], // x: unused, y: 1.0 - (hue / 360)
                          dragTo:    [0, 0.25],
                          param:     "h",
                          expected:  270,
                          tolerance: 1
                      });
                  });
                  it("should clip hue to min value", function () {
                      testDrag({
                          item:      "$hueSlider",
                          clickAt:   [0, 0.75], // x: unused, y: 1.0 - (hue / 360)
                          dragTo:    [0, 1.25],
                          param:     "h",
                          expected:  0,
                          tolerance: 1
                      });
                  });
                  it("should clip hue to max value", function () {
                      testDrag({
                          item:      "$hueSlider",
                          clickAt:   [0, 0.75], // x: unused, y: 1.0 - (hue / 360)
                          dragTo:    [0, -0.25],
                          param:     "h",
                          expected:  0,
                          tolerance: 1
                      });
                  });
                  it("should set opacity on mousedown", function () {
                      testMousedown({
                          item:      "$opacitySlider",
                          clickAt:   [0, 0.75], // x: unused, y: 1.0 - opacity
                          param:     "a",
                          expected:  0.25,
                          tolerance: 0.1
                      });
                  });
                  it("should set opacity on drag", function () {
                      testDrag({
                          item:      "$opacitySlider",
                          clickAt:   [0, 0.75], // x: unused, y: 1.0 - opacity
                          dragTo:    [0, 0.25],
                          param:     "a",
                          expected:  0.75,
                          tolerance: 0.1
                      });
                  });
                  it("should clip opacity to min value", function () {
                      testDrag({
                          item:      "$opacitySlider",
                          clickAt:   [0, 0.75], // x: unused, y: 1.0 - opacity
                          dragTo:    [0, 1.25],
                          param:     "a",
                          expected:  0,
                          tolerance: 0.1
                      });
                  });
                  it("should clip opacity to max value", function () {
                      // A increases going up, so a clientY at -0.25 of the item's height corresponds to >100%.
                      testDrag({
                          item:      "$opacitySlider",
                          clickAt:   [0, 0.75], // x: unused, y: 1.0 - opacity
                          dragTo:    [0, -0.25],
                          param:     "a",
                          expected:  1,
                          tolerance: 0.1
                      });
                  });
                  
              });
              
              describe("parameter editing with keyboard", function () {
                  
                  function makeKeyEvent(opts) {
                      return $.Event("keydown", { keyCode: opts.key, shiftKey: !!opts.shift });
                  }
  
                  /**
                   * Test a key event on the given UI element.
                   * @param {object} opts The parameters to test:
                   *     color: An optional initial value to set in the ColorEditor. Defaults to "hsla(50, 25%, 50%, 0.5)".
                   *     item: The (string) name of the member of ColorEditor that references the element to test.
                   *     key: The KeyEvent key code to simulate.
                   *     shift: Optional boolean specifying whether to simulate the shift key being down (default false).
                   *     param: The (string) parameter whose value we're testing (h, s, v, or a).
                   *     delta: The expected change in value for the parameter.
                   *     tolerance: The tolerance in variation for the expected value.
                   *     exact: True to compare the actual values stored in the _hsv object, false (default) to
                   *          compare tinycolor's normalization of the color value.
                   */
                  function testKey(opts) {
                      
                      function getParam() {
                          if (opts.exact) {
                              var result = colorEditor._hsv[opts.param];
                              // Because of #2201, this is sometimes a string with a percentage value.
                              if (typeof result === "string" && result.charAt(result.length - 1) === "%") {
                                  result = Number(result.substr(0, result.length - 1));
                              }
                              return result;
                          } else {
                              return tinycolor(colorEditor.getColor()).toHsv()[opts.param];
                          }
                      }
                      
                      makeUI(opts.color || "hsla(50, 25%, 50%, 0.5)");
  
                      var before = getParam();
                      colorEditor[opts.item].trigger(makeKeyEvent(opts));
                      
                      var after = getParam();
                      checkNear(after, before + opts.delta, opts.tolerance);
                  }
                  
                  /**
                   * Test whether the given event's default is or isn't prevented on a given key.
                   * @param {object} opts The parameters to test:
                   *     color: An optional initial value to set in the ColorEditor. Defaults to "hsla(50, 25%, 50%, 0.5)".
                   *     item: The (string) name of the member of ColorEditor that references the element to test.
                   *     selection: An optional array ([start, end]) specifying the selection to set in the given element.
                   *     key: The KeyEvent key code to simulate.
                   *     shift: Optional boolean specifying whether to simulate the shift key being down (default false).
                   *     expected: Whether the default is expected to be prevented.
                   */
                  function testPreventDefault(opts) {
                      var event, $item;
                      
                      // The color picker needs to be displayed for this test; otherwise the
                      // selection won't be properly set, because you can only set the selection
                      // when the text field has focus.
                      makeUI(opts.color || "hsla(50, 25%, 50%, 0.5)", function () { }, defaultSwatches, false);
                      
                      $item = colorEditor[opts.item];
                      $item.focus();
                      if (opts.selection) {
                          $item[0].setSelectionRange(opts.selection[0], opts.selection[1]);
                      }
                      
                      event = makeKeyEvent(opts);
                      $item.trigger(event);
                      expect(event.isDefaultPrevented()).toBe(opts.expected);
                  }
                  
                  it("should increase saturation by 1.5% on right arrow", function () {
                      testKey({
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_RIGHT,
                          param:     "s",
                          delta:     0.015,
                          tolerance: 0.01
                      });
                  });
                  it("should clip max saturation on right arrow", function () {
                      testKey({
                          color:     "hsla(50, 100%, 50%, 0.5)",
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_RIGHT,
                          param:     "s",
                          delta:     0,
                          tolerance: 0.01
                      });
                  });
                  it("should increase saturation by 7.5% on shift right arrow", function () {
                      testKey({
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_RIGHT,
                          shift:     true,
                          param:     "s",
                          delta:     0.075,
                          tolerance: 0.01
                      });
                  });
                  it("should clip max saturation on shift right arrow", function () {
                      testKey({
                          color:     "hsla(50, 100%, 50%, 0.5)",
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_RIGHT,
                          shift:     true,
                          param:     "s",
                          delta:     0,
                          tolerance: 0.01
                      });
                  });
                  it("should decrease saturation by 1.5% on left arrow", function () {
                      testKey({
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_LEFT,
                          param:     "s",
                          delta:     -0.015,
                          tolerance: 0.01
                      });
                  });
                  it("should clip min saturation on left arrow", function () {
                      testKey({
                          color:     "hsla(50, 0%, 50%, 0.5)",
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_LEFT,
                          param:     "s",
                          delta:     0,
                          tolerance: 0.01
                      });
                  });
                  it("should decrease saturation by 7.5% on shift left arrow", function () {
                      testKey({
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_LEFT,
                          shift:     true,
                          param:     "s",
                          delta:     -0.075,
                          tolerance: 0.01
                      });
                  });
                  it("should clip min saturation on shift left arrow", function () {
                      testKey({
                          color:     "hsla(50, 0%, 50%, 0.5)",
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_LEFT,
                          shift:     true,
                          param:     "s",
                          delta:     0,
                          tolerance: 0.01
                      });
                  });
                  it("should increase value by 1.5% on up arrow", function () {
                      testKey({
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_UP,
                          param:     "v",
                          delta:     0.015,
                          tolerance: 0.01
                      });
                  });
                  it("should clip max value on up arrow", function () {
                      testKey({
                          color:     "hsla(50, 25%, 100%, 0.5)",
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_UP,
                          param:     "v",
                          delta:     0,
                          tolerance: 0.01
                      });
                  });
                  it("should increase value by 7.5% on shift up arrow", function () {
                      testKey({
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_UP,
                          shift:     true,
                          param:     "v",
                          delta:     0.075,
                          tolerance: 0.01
                      });
                  });
                  it("should clip max value on shift up arrow", function () {
                      testKey({
                          color:     "hsla(50, 25%, 100%, 0.5)",
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_UP,
                          shift:     true,
                          param:     "v",
                          delta:     0,
                          tolerance: 0.01
                      });
                  });
                  it("should decrease value by 1.5% on down arrow", function () {
                      testKey({
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_DOWN,
                          param:     "v",
                          delta:     -0.015,
                          tolerance: 0.01
                      });
                  });
                  it("should clip min value on down arrow", function () {
                      testKey({
                          color:     "hsla(50, 25%, 0%, 0.5)",
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_DOWN,
                          param:     "v",
                          delta:     0,
                          tolerance: 0.01
                      });
                  });
                  it("should decrease value by 7.5% on shift down arrow", function () {
                      testKey({
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_DOWN,
                          shift:     true,
                          param:     "v",
                          delta:     -0.075,
                          tolerance: 0.01
                      });
                  });
                  it("should clip min value on shift down arrow", function () {
                      testKey({
                          color:     "hsla(50, 25%, 0%, 0.5)",
                          item:      "$selectionBase",
                          key:       KeyEvent.DOM_VK_DOWN,
                          shift:     true,
                          param:     "v",
                          delta:     0,
                          tolerance: 0.01
                      });
                  });
                  it("should increase hue by 3.6 on up arrow", function () {
                      testKey({
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_UP,
                          param:     "h",
                          delta:     3.6,
                          tolerance: 1
                      });
                  });
                  it("should wrap around max hue on up arrow", function () {
                      testKey({
                          color:     "hsla(359, 25%, 50%, 0.5)",
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_UP,
                          param:     "h",
                          delta:     -359 + 3.6,
                          tolerance: 1
                      });
                  });
                  it("should increase hue by 18 on shift up arrow", function () {
                      testKey({
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_UP,
                          shift:     true,
                          param:     "h",
                          delta:     18,
                          tolerance: 1
                      });
                  });
                  it("should wrap around max hue on shift up arrow", function () {
                      testKey({
                          color:     "hsla(359, 25%, 50%, 0.5)",
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_UP,
                          shift:     true,
                          param:     "h",
                          delta:     -359 + 18,
                          tolerance: 1
                      });
                  });
                  it("should decrease hue by 3.6 on down arrow", function () {
                      testKey({
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_DOWN,
                          param:     "h",
                          delta:     -3.6,
                          tolerance: 1
                      });
                  });
                  it("should wrap around min hue on down arrow", function () {
                      testKey({
                          color:     "hsla(0, 25%, 50%, 0.5)",
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_DOWN,
                          param:     "h",
                          delta:     360 - 3.6,
                          tolerance: 1
                      });
                  });
                  it("should decrease hue by 18 on shift down arrow", function () {
                      testKey({
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_DOWN,
                          shift:     true,
                          param:     "h",
                          delta:     -18,
                          tolerance: 1
                      });
                  });
                  it("should wrap around min hue on shift down arrow", function () {
                      testKey({
                          color:     "hsla(0, 25%, 50%, 0.5)",
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_DOWN,
                          shift:     true,
                          param:     "h",
                          delta:     360 - 18,
                          tolerance: 1
                      });
                  });
                  it("should increase opacity by 0.01 on up arrow", function () {
                      testKey({
                          item:      "$opacitySelector",
                          key:       KeyEvent.DOM_VK_UP,
                          param:     "a",
                          delta:     0.01,
                          tolerance: 0.005
                      });
                  });
                  it("should clip max opacity on up arrow", function () {
                      testKey({
                          color:     "hsla(90, 25%, 50%, 1.0)",
                          item:      "$opacitySelector",
                          key:       KeyEvent.DOM_VK_UP,
                          param:     "a",
                          delta:     0,
                          tolerance: 0.005
                      });
                  });
                  it("should increase opacity by 0.05 on shift up arrow", function () {
                      testKey({
                          item:      "$opacitySelector",
                          key:       KeyEvent.DOM_VK_UP,
                          shift:     true,
                          param:     "a",
                          delta:     0.05,
                          tolerance: 0.005
                      });
                  });
                  it("should clip max opacity on shift up arrow", function () {
                      testKey({
                          color:     "hsla(90, 25%, 50%, 1.0)",
                          item:      "$opacitySelector",
                          key:       KeyEvent.DOM_VK_UP,
                          shift:     true,
                          param:     "a",
                          delta:     0,
                          tolerance: 0.005
                      });
                  });
                  it("should decrease opacity by 0.01 on down arrow", function () {
                      testKey({
                          item:      "$opacitySelector",
                          key:       KeyEvent.DOM_VK_DOWN,
                          param:     "a",
                          delta:     -0.01,
                          tolerance: 0.005
                      });
                  });
                  it("should clip min opacity on down arrow", function () {
                      testKey({
                          color:     "hsla(90, 25%, 50%, 0)",
                          item:      "$opacitySelector",
                          key:       KeyEvent.DOM_VK_DOWN,
                          param:     "a",
                          delta:     0,
                          tolerance: 0.005
                      });
                  });
                  it("should decrease opacity by 0.05 on shift down arrow", function () {
                      testKey({
                          item:      "$opacitySelector",
                          key:       KeyEvent.DOM_VK_DOWN,
                          shift:     true,
                          param:     "a",
                          delta:     -0.05,
                          tolerance: 0.005
                      });
                  });
                  it("should clip min opacity on shift down arrow", function () {
                      testKey({
                          color:     "hsla(90, 25%, 50%, 0)",
                          item:      "$opacitySelector",
                          key:       KeyEvent.DOM_VK_DOWN,
                          shift:     true,
                          param:     "a",
                          delta:     0,
                          tolerance: 0.005
                      });
                  });
                  
                  // For #2138
                  it("should increase hue by 18 on shift up arrow even if saturation is 0", function () {
                      testKey({
                          color:     "hsl(180, 0, 0)",
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_UP,
                          shift:     true,
                          param:     "h",
                          delta:     18,
                          tolerance: 1,
                          exact:     true
                      });
                  });
                  it("should increase hue by 18 on shift up arrow for a near-gray hex color", function () {
                      testKey({
                          color:     "#5c5b56",
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_UP,
                          shift:     true,
                          param:     "h",
                          delta:     18,
                          tolerance: 1,
                          exact:     true
                      });
                  });
                  it("should not change value when hue changes", function () {
                      testKey({
                          color:     "#8e8247",
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_UP,
                          shift:     true,
                          param:     "v",
                          delta:     0,
                          tolerance: 0.01,
                          exact:     true
                      });
                  });
                  
                  // For #2193 and #2229
                  it("should prevent default on the key event for an unhandled arrow key on non-text-field", function () {
                      testPreventDefault({
                          color:     "#8e8247",
                          item:      "$hueBase",
                          key:       KeyEvent.DOM_VK_RIGHT,
                          expected:  true
                      });
                  });
                  it("should prevent default on left arrow at the start of the text field", function () {
                      testPreventDefault({
                          color:     "#8e8247",
                          item:      "$colorValue",
                          selection: [0, 0],
                          key:       KeyEvent.DOM_VK_LEFT,
                          expected:  true
                      });
                  });
                  it("should not prevent default on left arrow in the middle of the text field", function () {
                      testPreventDefault({
                          color:     "#8e8247",
                          item:      "$colorValue",
                          selection: [3, 3],
                          key:       KeyEvent.DOM_VK_LEFT,
                          expected:  false
                      });
                  });
                  it("should not prevent default on left arrow at the end of the text field", function () {
                      testPreventDefault({
                          color:     "#8e8247",
                          item:      "$colorValue",
                          selection: [7, 7],
                          key:       KeyEvent.DOM_VK_LEFT,
                          expected:  false
                      });
                  });
                  it("should not prevent default on left arrow with a range selection", function () {
                      testPreventDefault({
                          color:     "#8e8247",
                          item:      "$colorValue",
                          selection: [0, 7],
                          key:       KeyEvent.DOM_VK_LEFT,
                          expected:  false
                      });
                  });
                  it("should not prevent default on right arrow at the start of the text field", function () {
                      testPreventDefault({
                          color:     "#8e8247",
                          item:      "$colorValue",
                          selection: [0, 0],
                          key:       KeyEvent.DOM_VK_RIGHT,
                          expected:  false
                      });
                  });
                  it("should not prevent default on right arrow in the middle of the text field", function () {
                      testPreventDefault({
                          color:     "#8e8247",
                          item:      "$colorValue",
                          selection: [3, 3],
                          key:       KeyEvent.DOM_VK_RIGHT,
                          expected:  false
                      });
                  });
                  it("should prevent default on right arrow at the end of the text field", function () {
                      testPreventDefault({
                          color:     "#8e8247",
                          item:      "$colorValue",
                          selection: [7, 7],
                          key:       KeyEvent.DOM_VK_RIGHT,
                          expected:  true
                      });
                  });
                  it("should not prevent default on right arrow with a range selection", function () {
                      testPreventDefault({
                          color:     "#8e8247",
                          item:      "$colorValue",
                          selection: [0, 7],
                          key:       KeyEvent.DOM_VK_RIGHT,
                          expected:  false
                      });
                  });
  
              });
              
              describe("color swatches and original color", function () {
                  
                  it("should restore to original color when clicked on", function () {
                      makeUI("#abcdef");
                      colorEditor.setColorFromString("#0000ff");
                      colorEditor.$originalColor.trigger("click");
                      expect(tinycolor(colorEditor.getColor()).toHexString()).toBe("#abcdef");
                  });
                  
                  it("should create swatches", function () {
                      makeUI("#abcdef");
                      expect($(".swatch").length).toBe(2);
                  });
                  
                  it("should set color to a swatch when clicked on", function () {
                      makeUI("#fedcba");
                      $($(".swatch")[0]).trigger("click");
                      expect(tinycolor(colorEditor.getColor()).toHexString()).toBe("#abcdef");
                  });
                  
              });
              
              describe("input text field syncing", function () {
                  
                  it("should commit valid changes made in the input field on the input event", function () {
                      makeUI("#abcdef");
                      colorEditor.$colorValue.val("#fedcba");
                      colorEditor.$colorValue.trigger("input");
                      expect(tinycolor(colorEditor.getColor()).toHexString()).toBe("#fedcba");
                  });
                  it("should commit valid changes made in the input field on the change event", function () {
                      makeUI("#abcdef");
                      colorEditor.$colorValue.val("#fedcba");
                      colorEditor.$colorValue.trigger("change");
                      expect(tinycolor(colorEditor.getColor()).toHexString()).toBe("#fedcba");
                  });
                  it("should not commit changes on the input event while the value is invalid, but should keep them in the text field", function () {
                      makeUI("#abcdef");
                      colorEditor.$colorValue.val("rgb(0, 0, 0");
                      colorEditor.$colorValue.trigger("input");
                      expect(tinycolor(colorEditor.getColor()).toHexString()).toBe("#abcdef");
                      expect(colorEditor.$colorValue.val()).toBe("rgb(0, 0, 0");
                  });
                  it("should revert to the previous value on the change event while the value is invalid", function () {
                      makeUI("#abcdef");
                      colorEditor.$colorValue.val("rgb(0, 0, 0");
                      colorEditor.$colorValue.trigger("change");
                      expect(tinycolor(colorEditor.getColor()).toHexString()).toBe("#abcdef");
                      expect(colorEditor.$colorValue.val()).toBe("#abcdef");
                  });
                  
                  it("should convert percentage RGB values to normal values", function () {
                      makeUI("#abcdef");
                      expect(colorEditor._convertToNormalRGB("rgb(25%, 50%, 75%)")).toBe("rgb(64, 128, 191)");
                  });
                  it("should normalize a string to match tinycolor's format", function () {
                      makeUI("#abcdef");
                      //Percentage based colors are now supported: the following test is obsolete
                      //expect(colorEditor._normalizeColorString("rgb(25%,50%,75%)")).toBe("rgb(64, 128, 191)");
                      expect(colorEditor._normalizeColorString("rgb(10,20,   30)")).toBe("rgb(10, 20, 30)");
                  });
              });
              
              describe("undo/redo", function () {
                  
                  function triggerCtrlKey($element, key, shift) {
                      var ctrlKeyProperty = (brackets.platform === "win" ? "ctrlKey" : "metaKey"),
                          eventProps = {keyCode: key, shiftKey: shift};
                      eventProps[ctrlKeyProperty] = true;
                      $element.trigger($.Event("keydown", eventProps));
                  }
                  
                  it("should undo when Ctrl-Z is pressed on a focused element in the color editor", function () {
                      makeUI("#abcdef");
                      runs(function () {
                          colorEditor.setColorFromString("#a0a0a0");
                          colorEditor.$hueBase.focus();
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Z);
                          expect(getColorString()).toBe("#abcdef");
                      });
                  });
  
                  it("should redo when Ctrl-Shift-Z is pressed on a focused element in the color editor", function () {
                      makeUI("#abcdef");
                      runs(function () {
                          colorEditor._commitColor("#a0a0a0", true);
                          colorEditor.$hueBase.focus();
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Z);
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Z, true);
                          expect(getColorString()).toBe("#a0a0a0");
                      });
                  });
                  
                  it("should redo when Ctrl-Y is pressed on a focused element in the color editor", function () {
                      makeUI("#abcdef");
                      runs(function () {
                          colorEditor._commitColor("#a0a0a0", true);
                          colorEditor.$hueBase.focus();
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Z);
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Y);
                          expect(getColorString()).toBe("#a0a0a0");
                      });
                  });
                  
                  it("should redo when Ctrl-Y is pressed after two Ctrl-Zs (only one Ctrl-Z should take effect)", function () {
                      makeUI("#abcdef");
                      runs(function () {
                          colorEditor._commitColor("#a0a0a0", true);
                          colorEditor.$hueBase.focus();
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Z);
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Z);
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Y);
                          expect(getColorString()).toBe("#a0a0a0");
                      });
                  });
  
                  it("should undo when Ctrl-Z is pressed after two Ctrl-Ys (only one Ctrl-Y should take effect)", function () {
                      makeUI("#abcdef");
                      runs(function () {
                          colorEditor._commitColor("#a0a0a0", true);
                          colorEditor.$hueBase.focus();
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Z);
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Y);
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Y);
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Z);
                          expect(getColorString()).toBe("#abcdef");
                      });
                  });
  
                  it("should undo an rgba conversion", function () {
                      makeUI("#abcdef");
                      runs(function () {
                          colorEditor.$rgbaButton.click();
                          triggerCtrlKey(colorEditor.$rgbaButton, KeyEvent.DOM_VK_Z);
                          expect(getColorString()).toBe("#abcdef");
                      });
                  });
                  it("should undo an hsla conversion", function () {
                      makeUI("#abcdef");
                      runs(function () {
                          colorEditor.$hslButton.click();
                          triggerCtrlKey(colorEditor.$hslButton, KeyEvent.DOM_VK_Z);
                          expect(getColorString()).toBe("#abcdef");
                      });
                  });
                  it("should undo a hex conversion", function () {
                      makeUI("rgba(12, 32, 65, 0.2)");
                      runs(function () {
                          colorEditor.$hexButton.trigger("click");
                          triggerCtrlKey(colorEditor.$hexButton, KeyEvent.DOM_VK_Z);
                          expect(getColorString()).toBe("rgba(12, 32, 65, 0.2)");
                      });
                  });
  
                  it("should undo a saturation/value change", function () {
                      makeUI("rgba(100, 150, 200, 0.3)");
                      runs(function () {
                          eventAtRatio("mousedown", colorEditor.$selectionBase, [0.5, 0.5]);
                          triggerCtrlKey(colorEditor.$selectionBase, KeyEvent.DOM_VK_Z);
                          expect(getColorString()).toBe("rgba(100, 150, 200, 0.3)");
                          colorEditor.$selectionBase.trigger("mouseup");  // clean up drag state
                      });
                  });
                  it("should undo a hue change", function () {
                      makeUI("rgba(100, 150, 200, 0.3)");
                      runs(function () {
                          eventAtRatio("mousedown", colorEditor.$hueBase, [0, 0.5]);
                          triggerCtrlKey(colorEditor.$hueBase, KeyEvent.DOM_VK_Z);
                          expect(getColorString()).toBe("rgba(100, 150, 200, 0.3)");
                          colorEditor.$hueBase.trigger("mouseup");  // clean up drag state
                      });
                  });
                  it("should undo an opacity change", function () {
                      makeUI("rgba(100, 150, 200, 0.3)");
                      runs(function () {
                          eventAtRatio("mousedown", colorEditor.$opacitySelector, [0, 0.5]);
                          triggerCtrlKey(colorEditor.$opacitySelector, KeyEvent.DOM_VK_Z);
                          expect(getColorString()).toBe("rgba(100, 150, 200, 0.3)");
                          colorEditor.$opacitySelector.trigger("mouseup");  // clean up drag state
                      });
                  });
                  
                  it("should undo a text field change", function () {
                      makeUI("rgba(100, 150, 200, 0.3)");
                      runs(function () {
                          colorEditor.$colorValue.val("rgba(50, 50, 50, 0.9)");
                          triggerCtrlKey(colorEditor.$colorValue, KeyEvent.DOM_VK_Z);
                          expect(getColorString()).toBe("rgba(100, 150, 200, 0.3)");
                      });
                  });
                  it("should undo a swatch click", function () {
                      makeUI("rgba(100, 150, 200, 0.3)");
                      runs(function () {
                          var $swatch = $(colorEditor.$swatches.find("li")[0]);
                          $swatch.trigger("click");
                          triggerCtrlKey($swatch, KeyEvent.DOM_VK_Z);
                          expect(getColorString()).toBe("rgba(100, 150, 200, 0.3)");
                      });
                  });
  
              });
          });
      });
  });
  