Bug 769359 - Adds step attribute and stepUp/stepDown method support for <input type='date'>. r=mounir
authorRaphael Catolino <rcatolino@mozilla.com>
Sat, 22 Dec 2012 15:46:19 +0100
changeset 126220 3ff974766a3e324a7a10227613239c58ebecf7c2
parent 126219 5705bb322879b3f0ca5b047ce84bb0f63e4732bb
child 126221 0bb4773db082c0ecf241e9dac3d8ca3402efbb13
push id2151
push userlsblakk@mozilla.com
push dateTue, 19 Feb 2013 18:06:57 +0000
treeherdermozilla-beta@4952e88741ec [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmounir
bugs769359
milestone20.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 769359 - Adds step attribute and stepUp/stepDown method support for <input type='date'>. r=mounir
content/html/content/src/nsHTMLInputElement.cpp
content/html/content/src/nsHTMLInputElement.h
content/html/content/test/forms/test_step_attribute.html
content/html/content/test/forms/test_stepup_stepdown.html
--- a/content/html/content/src/nsHTMLInputElement.cpp
+++ b/content/html/content/src/nsHTMLInputElement.cpp
@@ -83,16 +83,17 @@
 #include "nsContentCreatorFunctions.h"
 #include "nsContentUtils.h"
 #include "mozilla/dom/DirectionalityUtils.h"
 #include "nsRadioVisitor.h"
 
 #include "mozilla/LookAndFeel.h"
 #include "mozilla/Util.h" // DebugOnly
 #include "mozilla/Preferences.h"
+#include "mozilla/MathAlgorithms.h"
 
 #include "nsIIDNService.h"
 
 #include <limits>
 
 // input type=date
 #include "jsapi.h"
 
@@ -171,16 +172,18 @@ static const nsAttrValue::EnumTable kInp
   { "titlecase", NS_INPUT_INPUTMODE_TITLECASE },
   { "autocapitalized", NS_INPUT_INPUTMODE_AUTOCAPITALIZED },
   { 0 }
 };
 
 // Default inputmode value is "auto".
 static const nsAttrValue::EnumTable* kInputDefaultInputmode = &kInputInputmodeTable[0];
 
+const double nsHTMLInputElement::kStepScaleFactorDate = 86400000;
+const double nsHTMLInputElement::kStepScaleFactorNumber = 1;
 const double nsHTMLInputElement::kDefaultStepBase = 0;
 const double nsHTMLInputElement::kStepAny = 0;
 
 #define NS_INPUT_ELEMENT_STATE_IID                 \
 { /* dc3b3d14-23e2-4479-b513-7b369343e3a0 */       \
   0xdc3b3d14,                                      \
   0x23e2,                                          \
   0x4479,                                          \
@@ -1196,53 +1199,64 @@ nsHTMLInputElement::GetList(nsIDOMHTMLEl
   CallQueryInterface(element, aValue);
   return NS_OK;
 }
 
 void
 nsHTMLInputElement::SetValue(double aValue)
 {
   nsAutoString value;
+  ConvertNumberToString(aValue, value);
+  SetValue(value);
+}
+
+bool
+nsHTMLInputElement::ConvertNumberToString(double aValue,
+                                          nsAString& aResultString) const
+{
+  MOZ_ASSERT(mType == NS_FORM_INPUT_DATE || mType == NS_FORM_INPUT_NUMBER,
+             "ConvertNumberToString is only implemented for type='{number,date}'");
+
+  aResultString.Truncate();
+
   switch (mType) {
     case NS_FORM_INPUT_NUMBER:
-      value.AppendFloat(aValue);
-      break;
+      aResultString.AppendFloat(aValue);
+      return true;
     case NS_FORM_INPUT_DATE:
-    {
-      value.Truncate();
-      JSContext* ctx = nsContentUtils::GetContextFromDocument(OwnerDoc());
-      if (!ctx) {
-        break;
-      }
-
-      JSObject* date = JS_NewDateObjectMsec(ctx, aValue);
-      if (!date) {
-        break;
+      {
+        JSContext* ctx = nsContentUtils::GetContextFromDocument(OwnerDoc());
+        if (!ctx) {
+          return false;
+        }
+
+        // The specs require |aValue| to be truncated.
+        aValue = floor(aValue);
+
+        JSObject* date = JS_NewDateObjectMsec(ctx, aValue);
+        if (!date) {
+          return false;
+        }
+
+        jsval year, month, day;
+        if (!JS::Call(ctx, date, "getUTCFullYear", 0, nullptr, &year) ||
+            !JS::Call(ctx, date, "getUTCMonth", 0, nullptr, &month) ||
+            !JS::Call(ctx, date, "getUTCDate", 0, nullptr, &day)) {
+          return false;
+        }
+
+        aResultString.AppendPrintf("%04.0f-%02.0f-%02.0f", year.toNumber(),
+                                   month.toNumber() + 1, day.toNumber());
+
+	return true;
       }
-
-      jsval year, month, day;
-      if(!JS::Call(ctx, date, "getUTCFullYear", 0, nullptr, &year)) {
-        break;
-      }
-
-      if(!JS::Call(ctx, date, "getUTCMonth", 0, nullptr, &month)) {
-        break;
-      }
-
-      if(!JS::Call(ctx, date, "getUTCDate", 0, nullptr, &day)) {
-        break;
-      }
-
-      value.AppendPrintf("%04.0f-%02.0f-%02.0f", year.toNumber(),
-                         month.toNumber() + 1, day.toNumber());
-    }
-    break;
-  }
-
-  SetValue(value);
+    default:
+      MOZ_NOT_REACHED();
+      return false;
+  }
 }
 
 NS_IMETHODIMP
 nsHTMLInputElement::GetValueAsDate(JSContext* aCtx, jsval* aDate)
 {
   if (mType != NS_FORM_INPUT_DATE) {
     aDate->setNull();
     return NS_OK;
@@ -1410,16 +1424,31 @@ nsHTMLInputElement::ApplyStep(int32_t aS
     } else if (aStep < 0) {
       value -= NS_floorModulo(value - GetStepBase(), step);
       value += step;
     }
   }
 
   value += aStep * step;
 
+  // For date inputs, the value can hold a string that is not a day. We do not
+  // want to round it, as it might result in a step mismatch. Instead we want to
+  // clamp to the next valid value.
+  if (mType == NS_FORM_INPUT_DATE &&
+      NS_floorModulo(value - GetStepBase(), GetStepScaleFactor()) != 0) {
+    double validStep = EuclidLCM<uint64_t>(static_cast<uint64_t>(step),
+                                           static_cast<uint64_t>(GetStepScaleFactor()));
+    if (aStep > 0) {
+      value -= NS_floorModulo(value - GetStepBase(), validStep);
+      value += validStep;
+    } else if (aStep < 0) {
+      value -= NS_floorModulo(value - GetStepBase(), validStep);
+    }
+  }
+
   // When stepUp() is called and the value is below min, we should clamp on
   // min unless stepUp() moves us higher than min.
   if (GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW) && aStep > 0 &&
       value <= min) {
     MOZ_ASSERT(!MOZ_DOUBLE_IS_NaN(min)); // min can't be NaN if we are here!
     value = min;
   // Same goes for stepDown() and max.
   } else if (GetValidityState(VALIDITY_STATE_RANGE_OVERFLOW) && aStep < 0 &&
@@ -4250,44 +4279,42 @@ nsHTMLInputElement::DoesMinMaxApply() co
       return false;
 #endif // DEBUG
   }
 }
 
 double
 nsHTMLInputElement::GetStep() const
 {
-  NS_ASSERTION(mType == NS_FORM_INPUT_NUMBER,
-               "We can't be there if type!=number!");
-
-  // NOTE: should be defaultStep * defaultStepScaleFactor,
-  // which is 1 for type=number.
+  MOZ_ASSERT(mType == NS_FORM_INPUT_NUMBER || mType == NS_FORM_INPUT_DATE,
+             "We can't be there if type!=number or date!");
+
+  // NOTE: should be defaultStep, which is 1 for type=number and date.
   double step = 1;
 
   if (HasAttr(kNameSpaceID_None, nsGkAtoms::step)) {
     nsAutoString stepStr;
     GetAttr(kNameSpaceID_None, nsGkAtoms::step, stepStr);
 
     if (stepStr.LowerCaseEqualsLiteral("any")) {
       // The element can't suffer from step mismatch if there is no step.
       return kStepAny;
     }
 
     nsresult ec;
-    // NOTE: should be multiplied by defaultStepScaleFactor,
-    // which is 1 for type=number.
     step = stepStr.ToDouble(&ec);
     if (NS_FAILED(ec) || step <= 0) {
-      // NOTE: we should use defaultStep * defaultStepScaleFactor,
-      // which is 1 for type=number.
+      // NOTE: we should use defaultStep, which is 1 for type=number and date.
       step = 1;
     }
   }
 
-  return step;
+  // TODO: This multiplication can lead to inexact results, we should use a
+  // type that supports a better precision than double. Bug 783607.
+  return step * GetStepScaleFactor();
 }
 
 // nsIConstraintValidation
 
 NS_IMETHODIMP
 nsHTMLInputElement::SetCustomValidity(const nsAString& aError)
 {
   nsIConstraintValidation::SetCustomValidity(aError);
@@ -4468,16 +4495,25 @@ nsHTMLInputElement::HasStepMismatch() co
     return false;
   }
 
   double step = GetStep();
   if (step == kStepAny) {
     return false;
   }
 
+  if (mType == NS_FORM_INPUT_DATE) {
+    // The multiplication by the stepScaleFactor for date can easily lead
+    // to precision loss, since in most use cases this value should be
+    // an integer (millisecond precision), we can get rid of the precision
+    // loss by rounding step. This will however lead to erroneous results
+    // when step was intented to have a precision superior to a millisecond.
+    step = NS_round(step);
+  }
+
   // Value has to be an integral multiple of step.
   return NS_floorModulo(value - GetStepBase(), step) != 0;
 }
 
 void
 nsHTMLInputElement::UpdateTooLongValidityState()
 {
   // TODO: this code will be re-enabled with bug 613016 and bug 613019.
@@ -4743,35 +4779,45 @@ nsHTMLInputElement::GetValidationMessage
       nsXPIDLString message;
 
       double value = GetValueAsDouble();
       MOZ_ASSERT(!MOZ_DOUBLE_IS_NaN(value));
 
       double step = GetStep();
       MOZ_ASSERT(step != kStepAny);
 
+      // In case this is a date and the step is not an integer, we don't want to
+      // display the dates corresponding to the truncated timestamps of valueLow
+      // and valueHigh because they might suffer from a step mismatch as well.
+      // Instead we want the timestamps to correspond to a rounded day. That is,
+      // we want a multiple of the step scale factor (1 day) as well as of step.
+      if (mType == NS_FORM_INPUT_DATE) {
+        step = EuclidLCM<uint64_t>(static_cast<uint64_t>(step),
+                                   static_cast<uint64_t>(GetStepScaleFactor()));
+      }
+
       double stepBase = GetStepBase();
 
       double valueLow = value - NS_floorModulo(value - stepBase, step);
       double valueHigh = value + step - NS_floorModulo(value - stepBase, step);
 
       double max = GetMaxAsDouble();
 
       if (MOZ_DOUBLE_IS_NaN(max) || valueHigh <= max) {
         nsAutoString valueLowStr, valueHighStr;
-        valueLowStr.AppendFloat(valueLow);
-        valueHighStr.AppendFloat(valueHigh);
+        ConvertNumberToString(valueLow, valueLowStr);
+        ConvertNumberToString(valueHigh, valueHighStr);
 
         const PRUnichar* params[] = { valueLowStr.get(), valueHighStr.get() };
         rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES,
                                                    "FormValidationStepMismatch",
                                                    params, message);
       } else {
         nsAutoString valueLowStr;
-        valueLowStr.AppendFloat(valueLow);
+        ConvertNumberToString(valueLow, valueLowStr);
 
         const PRUnichar* params[] = { valueLowStr.get() };
         rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES,
                                                    "FormValidationStepMismatchWithoutMax",
                                                    params, message);
       }
 
       aValidationMessage = message;
@@ -5178,16 +5224,32 @@ nsHTMLInputElement::GetFilterFromAccept(
       }
       filter = tokenFilter;
     }
   }
 
   return filter;
 }
 
+double
+nsHTMLInputElement::GetStepScaleFactor() const
+{
+  MOZ_ASSERT(DoesStepApply());
+
+  switch (mType) {
+    case NS_FORM_INPUT_DATE:
+      return kStepScaleFactorDate;
+    case NS_FORM_INPUT_NUMBER:
+      return kStepScaleFactorNumber;
+    default:
+      MOZ_NOT_REACHED();
+      return MOZ_DOUBLE_NaN();
+  }
+}
+
 void
 nsHTMLInputElement::UpdateValidityUIBits(bool aIsFocused)
 {
   if (aIsFocused) {
     // If the invalid UI is shown, we should show it while focusing (and
     // update). Otherwise, we should not.
     mCanShowInvalidUI = !IsValid() && ShouldShowValidityUI();
 
--- a/content/html/content/src/nsHTMLInputElement.h
+++ b/content/html/content/src/nsHTMLInputElement.h
@@ -567,16 +567,29 @@ protected:
    * or parse a number string to its value if type=number.
    * @param aValue the string to be parsed.
    * @param aResultValue the timestamp as a double.
    * @result whether the parsing was successful.
    */
   bool ConvertStringToNumber(nsAString& aValue, double& aResultValue) const;
 
   /**
+   * Convert a double to a string in a type specific way, ie convert a timestamp
+   * to a date string if type=date or append the number string representing the
+   * value if type=number.
+   *
+   * @param aValue the double to be converted
+   * @param aResultString [out] the string representing the double
+   * @return whether the function succeded, it will fail if the current input's
+   *         type is not supported or the number can't be converted to a string
+   *         as expected by the type.
+   */
+  bool ConvertNumberToString(double aValue, nsAString& aResultString) const;
+
+  /**
    * Parse a date string of the form yyyy-mm-dd
    * @param the string to be parsed.
    * @return whether the string is a valid date.
    * Note : this function does not consider the empty string as valid.
    */
   bool IsValidDate(nsAString& aValue) const;
 
   /**
@@ -614,16 +627,23 @@ protected:
   double GetMinAsDouble() const;
 
   /**
    * Returns the max attribute as a double.
    * Returns NaN if the max attribute isn't a valid floating point number.
    */
   double GetMaxAsDouble() const;
 
+   /**
+    * Get the step scale value for the current type.
+    * See:
+    * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-input-element-attributes.html#concept-input-step-scale
+    */
+  double GetStepScaleFactor() const;
+
   /**
    * Returns the current step value.
    * Returns kStepAny if the current step is "any" string.
    *
    * @return the current step value.
    */
   double GetStep() const;
 
@@ -683,16 +703,20 @@ protected:
    * The value of the input element when first initialized and it is updated
    * when the element is either changed through a script, focused or dispatches   
    * a change event. This is to ensure correct future change event firing.
    * NB: This is ONLY applicable where the element is a text control. ie,
    * where type= "text", "email", "search", "tel", "url" or "password".
    */
   nsString mFocusedValue;  
 
+  // Step scale factor values, for input types that have one.
+  static const double kStepScaleFactorDate;
+  static const double kStepScaleFactorNumber;
+
   // Default step base value when a type do not have specific one.
   static const double kDefaultStepBase;
   // Float alue returned by GetStep() when the step attribute is set to 'any'.
   static const double kStepAny;
 
   /**
    * The type of this input (<input type=...>) as an integer.
    * @see nsIFormControl.h (specifically NS_FORM_INPUT_*)
--- a/content/html/content/test/forms/test_step_attribute.html
+++ b/content/html/content/test/forms/test_step_attribute.html
@@ -23,17 +23,17 @@ var types = [
   [ 'hidden',         false ],
   [ 'text',           false ],
   [ 'search',         false ],
   [ 'tel',            false ],
   [ 'url',            false ],
   [ 'email',          false ],
   [ 'password',       false ],
   [ 'datetime',       true,  true ],
-  [ 'date',           true,  true ],
+  [ 'date',           true ],
   [ 'month',          true,  true ],
   [ 'week',           true,  true ],
   [ 'time',           true,  true ],
   [ 'datetime-local', true,  true ],
   [ 'number',         true ],
   [ 'range',          true,  true ],
   [ 'color',          false, true ],
   [ 'checkbox',       false ],
@@ -110,16 +110,142 @@ for (var data of types) {
                    0666, 0);
     outStream.write("foo", 3);
     outStream.close();
 
     input.value = file.path;
     checkValidity(input, true, apply);
 
     file.remove(false);
+  } else if (input.type == 'date') {
+    // For date, the step is calulated on the timestamp since 1970-01-01
+    // which mean that for all dates prior to the epoch, this timestamp is < 0
+    // and the behavior might differ, therefore we have to test for these cases.
+
+    // When step is 1 every date is valid
+    input.value = '2012-07-05';
+    checkValidity(input, true, apply);
+
+    input.step = 'foo';
+    input.value = '1970-01-01';
+    checkValidity(input, true, apply);
+
+    input.step = '-1';
+    input.value = '1969-12-12';
+    checkValidity(input, true, apply);
+
+    input.removeAttribute('step');
+    input.value = '1500-01-01';
+    checkValidity(input, true, apply);
+
+    input.step = 'any';
+    checkValidity(input, true, apply);
+
+    input.step = 'aNy';
+    checkValidity(input, true, apply);
+
+    input.step = 'AnY';
+    checkValidity(input, true, apply);
+
+    input.step = 'ANY';
+    checkValidity(input, true, apply);
+
+    // When min is set to a valid date, there is a step base.
+    input.min = '2008-02-28';
+    input.step = '2';
+    input.value = '2008-03-01';
+    checkValidity(input, true, apply);
+
+    input.value = '2008-02-29';
+    checkValidity(input, false, apply, { low: "2008-02-28", high: "2008-03-01" });
+
+    input.min = '2008-02-27';
+    input.value = '2008-02-28';
+    checkValidity(input, false, apply, { low: "2008-02-27", high: "2008-02-29" });
+
+    input.min = '2009-02-27';
+    input.value = '2009-02-28';
+    checkValidity(input, false, apply, { low: "2009-02-27", high: "2009-03-01" });
+
+    input.min = '2009-02-01';
+    input.step = '1.1';
+    input.value = '2009-02-02';
+    checkValidity(input, false, apply, { low: "2009-02-01", high: "2009-02-12" });
+
+    // Without any step attribute the date is valid
+    input.removeAttribute('step');
+    checkValidity(input, true, apply);
+
+    input.min = '1950-01-01';
+    input.step = '366';
+    input.value = '1951-01-01';
+    checkValidity(input, false, apply, { low: "1950-01-01", high: "1951-01-02" });
+
+    input.min = '1951-01-01';
+    input.step = '365';
+    input.value = '1952-01-01';
+    checkValidity(input, true, apply);
+
+    input.step = '0.9';
+    input.value = '1951-01-02';
+    checkValidity(input, false, apply, { low: "1951-01-01", high: "1951-01-10" });
+
+    input.value = '1951-01-10'
+    checkValidity(input, true, apply);
+
+    input.step = '0.5';
+    input.value = '1951-01-02';
+    checkValidity(input, true, apply);
+
+    input.step = '1.5';
+    input.value = '1951-01-03';
+    checkValidity(input, false, apply, { low: "1951-01-01", high: "1951-01-04" });
+
+    input.value = '1951-01-08';
+    checkValidity(input, false, apply, { low: "1951-01-07", high: "1951-01-10" });
+
+    input.step = '3000';
+    input.min= '1968-01-01';
+    input.value = '1968-05-12';
+    checkValidity(input, false, apply, { low: "1968-01-01", high: "1976-03-19" });
+
+    input.value = '1971-01-01';
+    checkValidity(input, false, apply, { low: "1968-01-01", high: "1976-03-19" });
+
+    input.value = '1991-01-01';
+    checkValidity(input, false, apply, { low: "1984-06-05", high: "1992-08-22" });
+
+    input.value = '1984-06-05';
+    checkValidity(input, true, apply);
+
+    input.value = '1992-08-22';
+    checkValidity(input, true, apply);
+
+    input.step = '1.1';
+    input.min = '1991-01-01';
+    input.value = '1991-01-01';
+    checkValidity(input, true, apply);
+
+    input.value = '1991-01-02';
+    checkValidity(input, false, apply, { low: "1991-01-01", high: "1991-01-12" });
+
+    input.value = '1991-01-12';
+    checkValidity(input, true, apply);
+
+    input.step = '1.1';
+    input.min = '1969-12-20';
+    input.value = '1969-12-20';
+    checkValidity(input, true, apply);
+
+    input.value = '1969-12-21';
+    checkValidity(input, false, apply, { low: "1969-12-20", high: "1969-12-31" });
+
+    input.value = '1969-12-31';
+    checkValidity(input, true, apply);
+
   } else {
     // When step=0, the allowed step is 1.
     input.value = '1.2';
     checkValidity(input, false, apply, { low: 1, high: 2 });
 
     input.value = '1';
     checkValidity(input, true, apply);
 
--- a/content/html/content/test/forms/test_stepup_stepdown.html
+++ b/content/html/content/test/forms/test_stepup_stepdown.html
@@ -42,25 +42,25 @@ function checkAvailability()
     ["checkbox", false],
     ["radio", false],
     ["file", false],
     ["submit", false],
     ["image", false],
     ["reset", false],
     ["button", false],
     ["number", true],
+    ["date", true],
     // The next types have not been implemented but will fallback to "text"
     // which has the same value.
     ["color", false],
   ];
 
   var todoList =
   [
     ["datetime", true],
-    ["date", true],
     ["month", true],
     ["week", true],
     ["time", true],
     ["datetime-local", true],
     ["range", true],
   ];
 
   var element = document.createElement("input");
@@ -102,22 +102,23 @@ function checkAvailability()
     } catch (e) {
       exceptionCaught = true;
     }
     todo_is(exceptionCaught, !data[1],
             "stepUp() availability is not correct");
   }
 }
 
-function checkStepDownForNumber()
+function checkStepDown()
 {
-  // This testData is very similar to the one in checkStepUpForNumber
-  // with some changes relative to stepDown.
+  // This testData is very similar to the one in checkStepUp with some changes
+  // relative to stepDown.
   var testData = [
   /* Initial value | step | min | max | stepDown arg | final value | exception */
+  { type: 'number', data: [
     // Regular case.
     [ '1',   null,  null,  null,  null, '0',    false ],
     // Argument testing.
     [ '1',   null,  null,  null,  1,    '0',    false ],
     [ '9',   null,  null,  null,  9,    '0',    false ],
     [ '1',   null,  null,  null,  -1,   '2',    false ],
     [ '1',   null,  null,  null,  0,    '1',    false ],
     // Float values are rounded to integer (1.1 -> 1).
@@ -173,64 +174,134 @@ function checkStepDownForNumber()
     [ '',   null,   null,  null,  null, '',     false ],
     // With step = 'any'.
     [ '0',  'any',  null,  null,  1,    null,   true ],
     [ '0',  'ANY',  null,  null,  1,    null,   true ],
     [ '0',  'AnY',  null,  null,  1,    null,   true ],
     [ '0',  'aNy',  null,  null,  1,    null,   true ],
     // With @value = step base.
     [ '1',  '2',    null,  null,  null, '-1',   false ],
+  ]},
+  { type: 'date', data: [
+    // Regular case.
+    [ '2012-07-09',  null,  null,  null,  null, '2012-07-08',   false ],
+    // Argument testing.
+    [ '2012-07-09',  null,  null,  null,  1,    '2012-07-08',   false ],
+    [ '2012-07-09',  null,  null,  null,  5,    '2012-07-04',   false ],
+    [ '2012-07-09',  null,  null,  null,  -1,   '2012-07-10',   false ],
+    [ '2012-07-09',  null,  null,  null,  0,    '2012-07-09',   false ],
+    // Month/Year wrapping.
+    [ '2012-08-01',  null,  null,  null,  1,    '2012-07-31',   false ],
+    [ '1969-01-02',  null,  null,  null,  4,    '1968-12-29',   false ],
+    [ '1969-01-01',  null,  null,  null,  -365, '1970-01-01',   false ],
+    [ '2012-02-29',  null,  null,  null,  -1,   '2012-03-01',   false ],
+    // Float values are rounded to integer (1.1 -> 1).
+    [ '2012-01-02',  null,  null,  null,  1.1,  '2012-01-01',   false ],
+    [ '2012-01-02',  null,  null,  null,  1.9,  '2012-01-01',   false ],
+    // With step values.
+    [ '2012-01-03',  '0.5', null,  null,  null, '2012-01-02',   false ],
+    [ '2012-01-02',  '0.5', null,  null,  null, '2012-01-01',   false ],
+    [ '2012-01-01',  '2',   null,  null,  null, '2011-12-30',   false ],
+    [ '2012-01-02',  '0.25',null,  null,  4,    '2012-01-01',   false ],
+    [ '2012-01-15',  '1.1',  '2012-01-01', null,  1,    '2012-01-12',   false ],
+    [ '2012-01-12',  '1.1',  '2012-01-01', null,  2,    '2012-01-01',   false ],
+    [ '2012-01-23',  '1.1',  '2012-01-01', null,  10,   '2012-01-12',   false ],
+    [ '2012-01-23',  '1.1',  '2012-01-01', null,  11,   '2012-01-01',   false ],
+    [ '1968-01-12',  '1.1',  '1968-01-01', null,  8,    '1968-01-01',   false ],
+    // step = 0 isn't allowed (-> step = 1).
+    [ '2012-01-02',  '0',   null,  null,  null, '2012-01-01',   false ],
+    // step < 0 isn't allowed (-> step = 1).
+    [ '2012-01-02',  '-1',  null,  null,  null, '2012-01-01',   false ],
+    // step = NaN isn't allowed (-> step = 1).
+    [ '2012-01-02',  'foo', null,  null,  null, '2012-01-01',   false ],
+    // Min values testing.
+    [ '2012-01-03',  '1',    'foo',        null,  2,     '2012-01-01',  false ],
+    [ '2012-01-02',  '1',    '2012-01-01', null,  null,  '2012-01-01',  false ],
+    [ '2012-01-01',  '1',    '2012-01-01', null,  null,  '2012-01-01',  false ],
+    [ '2012-01-01',  '1',    '2012-01-10', null,  1,     '2012-01-01',  false ],
+    [ '2012-01-05',  '3',    '2012-01-01', null,  null,  '2012-01-04',  false ],
+    [ '1969-01-01',  '5',    '1969-01-01', '1969-01-02', null,  '1969-01-01',  false ],
+    // Max values testing.
+    [ '2012-01-02',  '1',    null,  'foo',         null,  '2012-01-01',  false ],
+    [ '2012-01-02',  null,   null,  '2012-01-05',  null,  '2012-01-01',  false ],
+    [ '2012-01-03',  null,   null,  '2012-01-03',  null,  '2012-01-02',  false ],
+    [ '2012-01-07',  null,   null,  '2012-01-04',  4,     '2012-01-03',  false ],
+    [ '2012-01-07',  '2',    null,  '2012-01-04',  3,     '2012-01-01',  false ],
+    // Step mismatch.
+    [ '2012-01-04',  '2',    '2012-01-01',  null,         null,  '2012-01-03',  false ],
+    [ '2012-01-06',  '2',    '2012-01-01',  null,         2,     '2012-01-03',  false ],
+    [ '2012-01-05',  '2',    '2012-01-04',  '2012-01-08', null,  '2012-01-04',  false ],
+    [ '1970-01-04',  '2',    null,          null,         null,  '1970-01-03',  false ],
+    [ '1970-01-09',  '3',    null,          null,         null,  '1970-01-07',  false ],
+    // Clamping.
+    [ '2012-05-01',  null,   null,          '2012-01-05', null,  '2012-01-05',  false ],
+    [ '1970-01-05',  '2',    '1970-01-02',  '1970-01-05', null,  '1970-01-04',  false ],
+    [ '1970-01-01',  '5',    '1970-01-02',  '1970-01-09', 10,    '1970-01-01',  false ],
+    [ '1970-01-07',  '5',    '1969-12-27',  '1970-01-06', 2,     '1970-01-01',  false ],
+    [ '1970-03-08',  '3',    '1970-02-01',  '1970-02-07', 15,    '1970-02-01',  false ],
+    [ '1970-01-10',  '3',    '1970-01-01',  '1970-01-06', 2,     '1970-01-04',  false ],
+    // value = "" (NaN).
+    [ '',   null,   null,  null,  null, '',    false ],
+    // With step = 'any'.
+    [ '2012-01-01',  'any',  null,  null,  1,  null,  true ],
+    [ '2012-01-01',  'ANY',  null,  null,  1,  null,  true ],
+    [ '2012-01-01',  'AnY',  null,  null,  1,  null,  true ],
+    [ '2012-01-01',  'aNy',  null,  null,  1,  null,  true ],
+  ]},
   ];
 
-  for (var data of testData) {
-    var element = document.createElement("input");
-    element.type = 'number';
-
-    if (data[0] != null) {
-      element.setAttribute('value', data[0]);
-    }
-
-    if (data[1] != null) {
-      element.step = data[1];
-    }
+  for (var test of testData) {
+    for (var data of test.data) {
+      var element = document.createElement("input");
+      element.type = test.type;
 
-    if (data[2] != null) {
-      element.min = data[2];
-    }
+      if (data[0] != null) {
+        element.setAttribute('value', data[0]);
+      }
 
-    if (data[3] != null) {
-      element.max = data[3];
-    }
+      if (data[1] != null) {
+        element.step = data[1];
+      }
 
-    var exceptionCaught = false;
-    try {
-      if (data[4] != null) {
-        element.stepDown(data[4]);
-      } else {
-        element.stepDown();
+      if (data[2] != null) {
+        element.min = data[2];
       }
 
-      is(element.value, data[5], "The value should be " + data[5]);
-    } catch (e) {
-      exceptionCaught = true;
-      is(element.value, data[0], e.name + "The value should not have changed");
-      is(e.name, 'InvalidStateError',
-         "It should be a InvalidStateError exception.");
-    } finally {
-      is(exceptionCaught, data[6], "exception status should be " + data[6]);
+      if (data[3] != null) {
+        element.max = data[3];
+      }
+
+      var exceptionCaught = false;
+      try {
+        if (data[4] != null) {
+          element.stepDown(data[4]);
+        } else {
+          element.stepDown();
+        }
+
+        is(element.value, data[5], "The value should be " + data[5]);
+      } catch (e) {
+        exceptionCaught = true;
+        is(element.value, data[0], e.name + "The value should not have changed");
+        is(e.name, 'InvalidStateError',
+           "It should be a InvalidStateError exception.");
+      } finally {
+        is(exceptionCaught, data[6], "exception status should be " + data[6]);
+      }
     }
   }
 }
 
-function checkStepUpForNumber()
+function checkStepUp()
 {
-  // This testData is very similar to the one in checkStepDownForNumber
-  // with some changes relative to stepUp.
+  // This testData is very similar to the one in checkStepDown with some changes
+  // relative to stepUp.
   var testData = [
   /* Initial value | step | min | max | stepUp arg | final value | exception */
+  { type: 'number', data: [
     // Regular case.
     [ '1',   null,  null,  null,  null, '2',   false ],
     // Argument testing.
     [ '1',   null,  null,  null,  1,    '2',   false ],
     [ '9',   null,  null,  null,  9,    '18',  false ],
     [ '1',   null,  null,  null,  -1,   '0',   false ],
     [ '1',   null,  null,  null,  0,    '1',   false ],
     // Float values are rounded to integer (1.1 -> 1).
@@ -283,65 +354,138 @@ function checkStepUpForNumber()
     [ '',   null,   null,  null,  null, '',    false ],
     // With step = 'any'.
     [ '0',  'any',  null,  null,  1,    null,  true ],
     [ '0',  'ANY',  null,  null,  1,    null,  true ],
     [ '0',  'AnY',  null,  null,  1,    null,  true ],
     [ '0',  'aNy',  null,  null,  1,    null,  true ],
     // With @value = step base.
     [ '1',  '2',    null,  null,  null, '3',   false ],
+  ]},
+  { type: 'date', data: [
+    // Regular case.
+    [ '2012-07-09',  null,  null,  null,  null, '2012-07-10',   false ],
+    // Argument testing.
+    [ '2012-07-09',  null,  null,  null,  1,    '2012-07-10',   false ],
+    [ '2012-07-09',  null,  null,  null,  9,    '2012-07-18',   false ],
+    [ '2012-07-09',  null,  null,  null,  -1,   '2012-07-08',   false ],
+    [ '2012-07-09',  null,  null,  null,  0,    '2012-07-09',   false ],
+    // Month/Year wrapping.
+    [ '2012-07-31',  null,  null,  null,  1,    '2012-08-01',   false ],
+    [ '1968-12-29',  null,  null,  null,  4,    '1969-01-02',   false ],
+    [ '1970-01-01',  null,  null,  null,  -365, '1969-01-01',   false ],
+    [ '2012-03-01',  null,  null,  null,  -1,   '2012-02-29',   false ],
+    // Float values are rounded to integer (1.1 -> 1).
+    [ '2012-01-01',  null,  null,  null,  1.1,  '2012-01-02',   false ],
+    [ '2012-01-01',  null,  null,  null,  1.9,  '2012-01-02',   false ],
+    // With step values.
+    [ '2012-01-01',  '0.5',  null,         null,  null, '2012-01-02',   false ],
+    [ '2012-01-01',  '0.5',  null,         null,  null, '2012-01-02',   false ],
+    [ '2012-01-01',  '2',    null,         null,  null, '2012-01-03',   false ],
+    [ '2012-01-01',  '0.25', null,         null,  4,    '2012-01-02',   false ],
+    [ '2012-01-01',  '1.1',  '2012-01-01', null,  1,    '2012-01-12',   false ],
+    [ '2012-01-01',  '1.1',  '2012-01-01', null,  2,    '2012-01-12',   false ],
+    [ '2012-01-01',  '1.1',  '2012-01-01', null,  10,   '2012-01-12',   false ],
+    [ '2012-01-01',  '1.1',  '2012-01-01', null,  11,   '2012-01-23',   false ],
+    [ '1968-01-01',  '1.1',  '1968-01-01', null,  8,    '1968-01-12',   false ],
+    // step = 0 isn't allowed (-> step = 1).
+    [ '2012-01-01',  '0',   null,  null,  null, '2012-01-02',   false ],
+    // step < 0 isn't allowed (-> step = 1).
+    [ '2012-01-01',  '-1',  null,  null,  null, '2012-01-02',   false ],
+    // step = NaN isn't allowed (-> step = 1).
+    [ '2012-01-01',  'foo', null,  null,  null, '2012-01-02',   false ],
+    // Min values testing.
+    [ '2012-01-01',  '1',   'foo',         null,  null,  '2012-01-02',  false ],
+    [ '2012-01-01',  null,  '2011-12-01',  null,  null,  '2012-01-02',  false ],
+    [ '2012-01-01',  null,  '2012-01-02',  null,  null,  '2012-01-02',  false ],
+    [ '2012-01-01',  null,  '2012-01-01',  null,  null,  '2012-01-02',  false ],
+    [ '2012-01-01',  null,  '2012-01-04',  null,  4,     '2012-01-05',  false ],
+    [ '2012-01-01',  '2',   '2012-01-04',  null,  3,     '2012-01-06',  false ],
+    // Max values testing.
+    [ '2012-01-01',  '1',    null,  'foo',        2,     '2012-01-03',  false ],
+    [ '2012-01-01',  '1',    null,  '2012-01-10', 1,     '2012-01-02',  false ],
+    [ '2012-01-02',  null,   null,  '2012-01-01', null,  '2012-01-02',  false ],
+    [ '2012-01-02',  null,   null,  '2012-01-02', null,  '2012-01-02',  false ],
+    [ '2012-01-02',  null,   null,  '2012-01-02', null,  '2012-01-02',  false ],
+    [ '1969-01-02',  '5',    '1969-01-01',  '1969-01-02', null,  '1969-01-02',  false ],
+    // Step mismatch.
+    [ '2012-01-02',  '2',    '2012-01-01',  null,         null,  '2012-01-03',  false ],
+    [ '2012-01-02',  '2',    '2012-01-01',  null,         2,     '2012-01-05',  false ],
+    [ '2012-01-05',  '2',    '2012-01-01',  '2012-01-06', null,  '2012-01-05',  false ],
+    [ '1970-01-02',  '2',    null,          null,         null,  '1970-01-03',  false ],
+    [ '1970-01-05',  '3',    null,          null,         null,  '1970-01-07',  false ],
+    [ '1970-01-03',  '3',    null,          null,         null,  '1970-01-04',  false ],
+    [ '1970-01-03',  '3',    '1970-01-02',  null,         null,  '1970-01-05',  false ],
+    // Clamping.
+    [ '2012-01-01',  null,   '2012-01-31',  null,         null,  '2012-01-31',  false ],
+    [ '1970-01-02',  '2',    '1970-01-01',  '1970-01-04', null,  '1970-01-03',  false ],
+    [ '1970-01-01',  '5',    '1970-01-02',  '1970-01-09', 10,    '1970-01-07',  false ],
+    [ '1969-12-28',  '5',    '1969-12-29',  '1970-01-06', 3,     '1970-01-03',  false ],
+    [ '1970-01-01',  '3',    '1970-02-01',  '1970-02-07', 15,    '1970-02-07',  false ],
+    [ '1970-01-01',  '3',    '1970-01-01',  '1970-01-06', 2,     '1970-01-04',  false ],
+    // value = "" (NaN).
+    [ '',   null,   null,  null,  null, '',    false ],
+    // With step = 'any'.
+    [ '2012-01-01',  'any',  null,  null,  1,  null,  true ],
+    [ '2012-01-01',  'ANY',  null,  null,  1,  null,  true ],
+    [ '2012-01-01',  'AnY',  null,  null,  1,  null,  true ],
+    [ '2012-01-01',  'aNy',  null,  null,  1,  null,  true ],
+  ]},
   ];
 
-  for (var data of testData) {
-    var element = document.createElement("input");
-    element.type = 'number';
-
-    if (data[0] != null) {
-      element.setAttribute('value', data[0]);
-    }
-
-    if (data[1] != null) {
-      element.step = data[1];
-    }
+  for (var test of testData) {
+    for (var data of test.data) {
+      var element = document.createElement("input");
+      element.type = test.type;
 
-    if (data[2] != null) {
-      element.min = data[2];
-    }
+      if (data[0] != null) {
+        element.setAttribute('value', data[0]);
+      }
 
-    if (data[3] != null) {
-      element.max = data[3];
-    }
+      if (data[1] != null) {
+        element.step = data[1];
+      }
 
-    var exceptionCaught = false;
-    try {
-      if (data[4] != null) {
-        element.stepUp(data[4]);
-      } else {
-        element.stepUp();
+      if (data[2] != null) {
+        element.min = data[2];
       }
 
-      is(element.value, data[5], "The value should be " + data[5]);
-    } catch (e) {
-      exceptionCaught = true;
-      is(element.value, data[0], e.name + "The value should not have changed");
-      is(e.name, 'InvalidStateError',
-         "It should be a InvalidStateError exception.");
-    } finally {
-      is(exceptionCaught, data[6], "exception status should be " + data[6]);
+      if (data[3] != null) {
+        element.max = data[3];
+      }
+
+      var exceptionCaught = false;
+      try {
+        if (data[4] != null) {
+          element.stepUp(data[4]);
+        } else {
+          element.stepUp();
+        }
+
+        is(element.value, data[5], "The value should be " + data[5]);
+      } catch (e) {
+        exceptionCaught = true;
+        is(element.value, data[0], e.name + "The value should not have changed");
+        is(e.name, 'InvalidStateError',
+           "It should be a InvalidStateError exception.");
+      } finally {
+        is(exceptionCaught, data[6], "exception status should be " + data[6]);
+      }
     }
   }
 }
 
 SimpleTest.waitForExplicitFinish();
 SpecialPowers.pushPrefEnv({'set': [["dom.experimental_forms", true]]}, function() {
 
 checkPresence();
 checkAvailability();
-checkStepDownForNumber();
-checkStepUpForNumber();
+
+checkStepDown();
+checkStepUp();
 
 SimpleTest.finish();
 });
 
 </script>
 </pre>
 </body>
 </html>