Bug 1328219 - (Part 2) Add browser chrome test for date picker. r=mconley draft
authorScott Wu <scottcwwu@gmail.com>
Thu, 03 Aug 2017 18:26:23 +0800
changeset 643964 b203d0a530a1325cb3bad16c81e3c71d0f7055bb
parent 643034 60597a0980c0a1b4c44af83c6c28a59d760307ea
child 725466 cae06b1f8061f059b1a32a77cf5952a17f1a7178
push id73281
push userbmo:scwwu@mozilla.com
push dateThu, 10 Aug 2017 10:07:31 +0000
reviewersmconley
bugs1328219
milestone57.0a1
Bug 1328219 - (Part 2) Add browser chrome test for date picker. r=mconley MozReview-Commit-ID: 99zfNaXqbjc
toolkit/content/tests/browser/browser.ini
toolkit/content/tests/browser/browser_datetime_datepicker.js
toolkit/content/tests/browser/head.js
--- a/toolkit/content/tests/browser/browser.ini
+++ b/toolkit/content/tests/browser/browser.ini
@@ -53,16 +53,17 @@ skip-if = !e10s
 [browser_bug594509.js]
 [browser_bug982298.js]
 [browser_charsetMenu_swapBrowsers.js]
 [browser_content_url_annotation.js]
 skip-if = !e10s || !crashreporter
 [browser_contentTitle.js]
 [browser_crash_previous_frameloader.js]
 run-if = e10s && crashreporter
+[browser_datetime_datepicker.js]
 [browser_default_image_filename.js]
 [browser_f7_caret_browsing.js]
 [browser_findbar.js]
 [browser_isSynthetic.js]
 [browser_keyevents_during_autoscrolling.js]
 [browser_label_textlink.js]
 [browser_mediaPlayback.js]
 tags = audiochannel
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_datetime_datepicker.js
@@ -0,0 +1,222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const MONTH_YEAR = ".month-year",
+      DAYS_VIEW = ".days-view",
+      BTN_PREV_MONTH = ".prev",
+      BTN_NEXT_MONTH = ".next";
+const DATE_FORMAT = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "long", timeZone: "UTC" }).format;
+
+// Create a list of abbreviations for calendar class names
+const W = "weekend",
+      O = "outside",
+      S = "selection",
+      R = "out-of-range",
+      T = "today",
+      P = "off-step";
+
+// Calendar classlist for 2016-12. Used to verify the classNames are correct.
+const calendarClasslist_201612 = [
+  [W, O], [O], [O], [O], [],  [],  [W],
+  [W],    [],  [],  [],  [],  [],  [W],
+  [W],    [],  [],  [],  [S], [],  [W],
+  [W],    [],  [],  [],  [],  [],  [W],
+  [W],    [],  [],  [],  [],  [],  [W],
+  [W, O], [O], [O], [O], [O], [O], [W, O],
+];
+
+function getCalendarText() {
+  return helper.getChildren(DAYS_VIEW).map(child => child.textContent);
+}
+
+function getCalendarClassList() {
+  return helper.getChildren(DAYS_VIEW).map(child => Array.from(child.classList));
+}
+
+function mergeArrays(a, b) {
+  return a.map((classlist, index) => classlist.concat(b[index]));
+}
+
+let helper = new DateTimeTestHelper();
+
+registerCleanupFunction(() => {
+  helper.cleanup();
+});
+
+/**
+ * Test that date picker opens to today's date when input field is blank
+ */
+add_task(async function test_datepicker_today() {
+  const date = new Date();
+
+  await helper.openPicker("data:text/html, <input type='date'>");
+
+  Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(date));
+
+  await helper.tearDown();
+});
+
+/**
+ * Test that date picker opens to the correct month, with calendar days
+ * displayed correctly, given a date value is set.
+ */
+add_task(async function test_datepicker_open() {
+  const inputValue = "2016-12-15";
+
+  await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
+
+  Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue)));
+  Assert.deepEqual(
+    getCalendarText(),
+    [
+      "27", "28", "29", "30",  "1",  "2",  "3",
+       "4",  "5",  "6",  "7",  "8",  "9", "10",
+      "11", "12", "13", "14", "15", "16", "17",
+      "18", "19", "20", "21", "22", "23", "24",
+      "25", "26", "27", "28", "29", "30", "31",
+       "1",  "2",  "3",  "4",  "5",  "6",  "7",
+    ],
+    "2016-12",
+  );
+  Assert.deepEqual(
+    getCalendarClassList(),
+    calendarClasslist_201612,
+    "2016-12 classNames"
+  );
+
+  await helper.tearDown();
+});
+
+/**
+ * When the prev month button is clicked, calendar should display the dates for
+ * the previous month.
+ */
+add_task(async function test_datepicker_prev_month_btn() {
+  const inputValue = "2016-12-15";
+  const prevMonth = "2016-11-01";
+
+  await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
+  helper.click(helper.getElement(BTN_PREV_MONTH));
+
+  Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(prevMonth)));
+  Assert.deepEqual(
+    getCalendarText(),
+    [
+      "30", "31",  "1",  "2",  "3",  "4",  "5",
+       "6",  "7",  "8",  "9", "10", "11", "12",
+      "13", "14", "15", "16", "17", "18", "19",
+      "20", "21", "22", "23", "24", "25", "26",
+      "27", "28", "29", "30",  "1",  "2",  "3",
+       "4",  "5",  "6",  "7",  "8",  "9", "10",
+    ],
+    "2016-11",
+  );
+
+  await helper.tearDown();
+});
+
+/**
+ * When the next month button is clicked, calendar should display the dates for
+ * the next month.
+ */
+add_task(async function test_datepicker_next_month_btn() {
+  const inputValue = "2016-12-15";
+  const nextMonth = "2017-01-01";
+
+  await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
+  helper.click(helper.getElement(BTN_NEXT_MONTH));
+
+  Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(nextMonth)));
+  Assert.deepEqual(
+    getCalendarText(),
+    [
+      "25", "26", "27", "28", "29", "30", "31",
+       "1",  "2",  "3",  "4",  "5",  "6",  "7",
+       "8",  "9", "10", "11", "12", "13", "14",
+      "15", "16", "17", "18", "19", "20", "21",
+      "22", "23", "24", "25", "26", "27", "28",
+      "29", "30", "31",  "1",  "2",  "3",  "4",
+    ],
+    "2017-01",
+  );
+
+  await helper.tearDown();
+});
+
+/**
+ * When a date on the calendar is clicked, date picker should close and set
+ * value to the input box.
+ */
+add_task(async function test_datepicker_clicked() {
+  const inputValue = "2016-12-15";
+  const firstDayOnCalendar = "2016-11-27";
+
+  await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
+  // Click the first item (top-left corner) of the calendar
+  helper.click(helper.getElement(DAYS_VIEW).children[0]);
+  await ContentTask.spawn(helper.tab.linkedBrowser, {}, async function() {
+    let inputEl = content.document.querySelector("input");
+    await ContentTaskUtils.waitForEvent(inputEl, "input");
+  });
+
+  Assert.equal(content.document.querySelector("input").value, firstDayOnCalendar);
+
+  await helper.tearDown();
+});
+
+/**
+ * When min and max attributes are set, calendar should show some dates as
+ * out-of-range.
+ */
+add_task(async function test_datepicker_min_max() {
+  const inputValue = "2016-12-15";
+  const inputMin = "2016-12-05";
+  const inputMax = "2016-12-25";
+
+  await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}" min="${inputMin}" max="${inputMax}">`);
+
+  Assert.deepEqual(
+    getCalendarClassList(),
+    mergeArrays(calendarClasslist_201612, [
+      // R denotes out-of-range
+      [R], [R], [R], [R], [R], [R], [R],
+      [R], [],  [],  [],  [],  [],  [],
+      [],  [],  [],  [],  [],  [],  [],
+      [],  [],  [],  [],  [],  [],  [],
+      [],  [R], [R], [R], [R], [R], [R],
+      [R], [R], [R], [R], [R], [R], [R],
+    ]),
+    "2016-12 with min & max",
+  );
+
+  await helper.tearDown();
+});
+
+/**
+ * When step attribute is set, calendar should show some dates as off-step.
+ */
+add_task(async function test_datepicker_step() {
+  const inputValue = "2016-12-15";
+  const inputStep = "5";
+
+  await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}" step="${inputStep}">`);
+
+  Assert.deepEqual(
+    getCalendarClassList(),
+    mergeArrays(calendarClasslist_201612, [
+      // P denotes off-step
+      [P], [P], [P], [],  [P], [P], [P],
+      [P], [],  [P], [P], [P], [P], [],
+      [P], [P], [P], [P], [],  [P], [P],
+      [P], [P], [],  [P], [P], [P], [P],
+      [],  [P], [P], [P], [P], [],  [P],
+      [P], [P], [P], [],  [P], [P], [P],
+    ]),
+    "2016-12 with step",
+  );
+
+  await helper.tearDown();
+});
--- a/toolkit/content/tests/browser/head.js
+++ b/toolkit/content/tests/browser/head.js
@@ -117,8 +117,94 @@ function hover_icon(icon, tooltip) {
 function leave_icon(icon) {
   EventUtils.synthesizeMouse(icon, 0, 0, {type: "mouseout"});
   EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
   EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
   EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
 
   disable_non_test_mouse(false);
 }
+
+/**
+ * Helper class for testing datetime input picker widget
+ */
+class DateTimeTestHelper {
+  constructor() {
+    this.panel = document.getElementById("DateTimePickerPanel");
+    this.tab = null;
+    this.frame = null;
+  }
+
+  /**
+   * Opens a new tab with the URL of the test page, and make sure the picker is
+   * ready for testing.
+   *
+   * @param  {String} pageUrl
+   */
+  async openPicker(pageUrl) {
+    this.tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+    await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, gBrowser.selectedBrowser);
+    // If dateTimePopupFrame doesn't exist yet, wait for the binding to be attached
+    if (!this.panel.dateTimePopupFrame) {
+      await BrowserTestUtils.waitForEvent(this.panel, "DateTimePickerBindingReady")
+    }
+    this.frame = this.panel.dateTimePopupFrame;
+    await BrowserTestUtils.waitForEvent(this.frame, "load", true);
+    // Wait for picker elements to be ready and open panel transition to end
+    await Promise.all([
+      BrowserTestUtils.waitForEvent(this.frame.contentDocument, "PickerReady"),
+      BrowserTestUtils.waitForEvent(this.panel, "transitionend"),
+    ]);
+  }
+
+  /**
+   * Find an element on the picker.
+   *
+   * @param  {String} selector
+   * @return {DOMElement}
+   */
+  getElement(selector) {
+    return this.frame.contentDocument.querySelector(selector);
+  }
+
+  /**
+   * Find the children of an element on the picker.
+   *
+   * @param  {String} selector
+   * @return {Array<DOMElement>}
+   */
+  getChildren(selector) {
+    return Array.from(this.getElement(selector).children);
+  }
+
+  /**
+   * Click on an element
+   *
+   * @param  {DOMElement} element
+   */
+  click(element) {
+    EventUtils.synthesizeMouseAtCenter(element, {}, this.frame.contentWindow);
+  }
+
+  /**
+   * Close the panel and the tab
+   */
+  async tearDown() {
+    if (!this.panel.hidden) {
+      let pickerClosePromise = new Promise(resolve => {
+        this.panel.addEventListener("popuphidden", resolve, {once: true});
+      });
+      this.panel.closePicker();
+      await pickerClosePromise;
+    }
+    await BrowserTestUtils.removeTab(this.tab);
+    this.tab = null;
+  }
+
+  /**
+   * Clean up after tests. Remove the frame to prevent leak.
+   */
+  cleanup() {
+    this.frame.remove();
+    this.frame = null;
+    this.panel = null;
+  }
+}