Bug 1422164 - Create a rich-select, address-option, and basic-card-option custom elements. r=MattN
☠☠ backed out by 5786d2b246ee ☠ ☠
authorJared Wein <jwein@mozilla.com>
Fri, 01 Dec 2017 14:38:27 -0500
changeset 452743 90da1b24e686f5350c9ab09720e1745eb2bdb286
parent 452742 79a6260e52a7f225e6893e7640980d04ebfcf87e
child 452744 7b813845b721a97bb5e2743912635c149f26039c
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1422164
milestone59.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 1422164 - Create a rich-select, address-option, and basic-card-option custom elements. r=MattN MozReview-Commit-ID: 7EtK5GEWNZd
toolkit/components/payments/jar.mn
toolkit/components/payments/res/components/address-option.css
toolkit/components/payments/res/components/address-option.js
toolkit/components/payments/res/components/basic-card-option.css
toolkit/components/payments/res/components/basic-card-option.js
toolkit/components/payments/res/components/rich-option.js
toolkit/components/payments/res/components/rich-select.css
toolkit/components/payments/res/components/rich-select.js
toolkit/components/payments/res/paymentRequest.xhtml
toolkit/components/payments/test/mochitest/mochitest.ini
toolkit/components/payments/test/mochitest/test_rich_select.html
--- a/toolkit/components/payments/jar.mn
+++ b/toolkit/components/payments/jar.mn
@@ -5,15 +5,16 @@
 toolkit.jar:
 %   content payments %content/payments/
     content/payments/paymentDialog.css                (content/paymentDialog.css)
     content/payments/paymentDialog.js                 (content/paymentDialog.js)
     content/payments/paymentDialogFrameScript.js      (content/paymentDialogFrameScript.js)
     content/payments/paymentDialog.xhtml              (content/paymentDialog.xhtml)
 %   resource payments %res/payments/
     res/payments                                      (res/paymentRequest.*)
+    res/payments/components/                          (res/components/*.css)
     res/payments/components/                          (res/components/*.js)
     res/payments/containers/                          (res/containers/*.js)
     res/payments/debugging.html                       (res/debugging.html)
     res/payments/debugging.js                         (res/debugging.js)
     res/payments/mixins/                              (res/mixins/*.js)
     res/payments/PaymentsStore.js                     (res/PaymentsStore.js)
     res/payments/vendor/                              (res/vendor/*)
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/address-option.css
@@ -0,0 +1,58 @@
+/* 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/. */
+
+address-option {
+  display: grid;
+  grid-row-gap: 5px;
+  grid-column-gap: 10px;
+  grid-template-areas:
+    "recipient  "
+    "addressLine";
+
+  border-bottom: 1px solid #ddd;
+  background: #fff;
+  padding: 5px;
+  padding-inline-start: 20px;
+  width: 400px;
+  font-size: .8em;
+}
+
+rich-select[open] > .rich-select-popup-box > address-option {
+  grid-template-areas:
+    "recipient   recipient"
+    "addressLine addressLine"
+    "email       phone      ";
+}
+
+address-option > .recipient {
+  grid-area: recipient;
+}
+
+address-option > .addressLine {
+  grid-area: addressLine;
+}
+
+address-option > .email {
+  grid-area: email;
+}
+
+address-option > .phone {
+  grid-area: phone;
+}
+
+address-option > .recipient,
+address-option > .addressLine,
+address-option > .email,
+address-option > .phone {
+  white-space: nowrap;
+}
+
+.rich-select-popup-box > address-option[selected] {
+  background-color: #ffa;
+}
+
+rich-select > .rich-select-selected-clone > .email,
+rich-select > .rich-select-selected-clone > .phone {
+  display: none;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/address-option.js
@@ -0,0 +1,79 @@
+/* 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/. */
+
+/**
+ * <rich-select>
+ *  <address-option addressLine="1234 Anywhere St"
+ *                  city="Some City"
+ *                  country="USA"
+ *                  dependentLocality=""
+ *                  languageCode="en-US"
+ *                  phone=""
+ *                  postalCode="90210"
+ *                  recipient="Jared Wein"
+ *                  region="MI"></address-option>
+ * </rich-select>
+ */
+
+/* global ObservedPropertiesMixin, RichOption */
+
+class AddressOption extends ObservedPropertiesMixin(RichOption) {
+  static get observedAttributes() {
+    return RichOption.observedAttributes.concat([
+      "addressLine",
+      "city",
+      "country",
+      "dependentLocality",
+      "email",
+      "languageCode",
+      "organization",
+      "phone",
+      "postalCode",
+      "recipient",
+      "region",
+      "sortingCode",
+    ]);
+  }
+
+  connectedCallback() {
+    for (let child of this.children) {
+      child.remove();
+    }
+
+    let fragment = document.createDocumentFragment();
+    let recipient = RichOption._createElement(fragment, "recipient");
+    let addressLine = RichOption._createElement(fragment, "addressLine");
+    let email = RichOption._createElement(fragment, "email");
+    let phone = RichOption._createElement(fragment, "phone");
+    this.appendChild(fragment);
+
+    this.elementMap = {
+      recipient,
+      addressLine,
+      email,
+      phone,
+    };
+
+    super.connectedCallback();
+  }
+
+  disconnectedCallback() {
+    this.elementMap = {};
+  }
+
+  render() {
+    if (!this.parentNode) {
+      return;
+    }
+
+    this.elementMap.recipient.textContent = this.recipient;
+    this.elementMap.addressLine.textContent =
+      `${this.addressLine} ${this.city} ${this.region} ${this.postalCode} ${this.country}`;
+    this.elementMap.email.textContent = this.email;
+    this.elementMap.phone.textContent = this.phone;
+  }
+}
+
+customElements.define("address-option", AddressOption);
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/basic-card-option.css
@@ -0,0 +1,56 @@
+/* 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/. */
+
+basic-card-option {
+  display: grid;
+  grid-row-gap: 5px;
+  grid-column-gap: 10px;
+  grid-template-areas:
+    "owner  type"
+    "number ...";
+
+  border-bottom: 1px solid #ddd;
+  background: #fff;
+  padding: 5px;
+  padding-inline-start: 20px;
+  width: 400px;
+  font-size: .8em;
+}
+
+rich-select[open] > .rich-select-popup-box > basic-card-option {
+  grid-template-areas:
+    "owner   type"
+    "number  expiration";
+}
+
+basic-card-option > .number {
+  grid-area: number;
+}
+
+basic-card-option > .owner {
+  grid-area: owner;
+}
+
+basic-card-option > .expiration {
+  grid-area: expiration;
+}
+
+basic-card-option > .type {
+  grid-area: type;
+}
+
+basic-card-option > .number,
+basic-card-option > .owner,
+basic-card-option > .expiration,
+basic-card-option > .type {
+  white-space: nowrap;
+}
+
+.rich-select-popup-box > basic-card-option[selected] {
+  background-color: #ffa;
+}
+
+rich-select > .rich-select-selected-clone > .expiration {
+  display: none;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/basic-card-option.js
@@ -0,0 +1,61 @@
+/* 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/. */
+
+/**
+ * <rich-select>
+ *  <basic-card-option></basic-card-option>
+ * </rich-select>
+ */
+
+/* global ObservedPropertiesMixin, RichOption */
+
+class BasicCardOption extends ObservedPropertiesMixin(RichOption) {
+  static get observedAttributes() {
+    return RichOption.observedAttributes.concat([
+      "expiration",
+      "number",
+      "owner",
+      "type",
+    ]);
+  }
+
+  connectedCallback() {
+    for (let child of this.children) {
+      child.remove();
+    }
+
+    let fragment = document.createDocumentFragment();
+    let owner = RichOption._createElement(fragment, "owner");
+    let number = RichOption._createElement(fragment, "number");
+    let expiration = RichOption._createElement(fragment, "expiration");
+    let type = RichOption._createElement(fragment, "type");
+    this.appendChild(fragment);
+
+    this.elementMap = {
+      owner,
+      number,
+      expiration,
+      type,
+    };
+
+    super.connectedCallback();
+  }
+
+  disconnectedCallback() {
+    this.elementMap = {};
+  }
+
+  render() {
+    if (!this.parentNode) {
+      return;
+    }
+
+    this.elementMap.owner.textContent = this.owner;
+    this.elementMap.number.textContent = this.number;
+    this.elementMap.expiration.textContent = this.expiration;
+    this.elementMap.type.textContent = this.type;
+  }
+}
+
+customElements.define("basic-card-option", BasicCardOption);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/rich-option.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+/**
+ * <rich-select>
+ *  <rich-option></rich-option>
+ * </rich-select>
+ */
+
+/* global ObservedPropertiesMixin */
+/* exported RichOption */
+
+class RichOption extends ObservedPropertiesMixin(HTMLElement) {
+  static get observedAttributes() { return ["selected", "hidden"]; }
+
+  constructor() {
+    super();
+
+    this.addEventListener("click", this);
+    this.addEventListener("keypress", this);
+  }
+
+  connectedCallback() {
+    this.render();
+    let richSelect = this.closest("rich-select");
+    if (richSelect && richSelect.render) {
+      richSelect.render();
+    }
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "click": {
+        this.onClick(event);
+        break;
+      }
+      case "keypress": {
+        this.onKeyPress(event);
+        break;
+      }
+    }
+  }
+
+  onClick(event) {
+    if (this.closest("rich-select").open &&
+        !this.disabled &&
+        event.button == 0) {
+      for (let option of this.parentNode.children) {
+        option.selected = option == this;
+      }
+    }
+  }
+
+  onKeyPress(event) {
+    if (!this.disabled &&
+        event.which == 13 /* Enter */) {
+      for (let option of this.parentNode.children) {
+        option.selected = option == this;
+      }
+    }
+  }
+
+  get selected() {
+    return this.hasAttribute("selected");
+  }
+
+  set selected(value) {
+    if (value) {
+      let oldSelectedOptions = this.parentNode.querySelectorAll("[selected]");
+      for (let option of oldSelectedOptions) {
+        option.removeAttribute("selected");
+      }
+      this.setAttribute("selected", value);
+    } else {
+      this.removeAttribute("selected");
+    }
+    let richSelect = this.closest("rich-select");
+    if (richSelect && richSelect.render) {
+      richSelect.render();
+    }
+    return value;
+  }
+
+  static _createElement(fragment, className) {
+    let element = document.createElement("span");
+    element.classList.add(className);
+    fragment.appendChild(element);
+    return element;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/rich-select.css
@@ -0,0 +1,17 @@
+/* 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/. */
+
+rich-select:not([open]) > .rich-select-popup-box {
+  display: none;
+}
+
+rich-select[open] {
+  position: relative;
+}
+
+rich-select[open] > .rich-select-popup-box {
+  position: absolute;
+  z-index: 1;
+  top: 1em;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/components/rich-select.js
@@ -0,0 +1,160 @@
+/* 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/. */
+
+/**
+ * <rich-select>
+ *  <rich-option></rich-option>
+ * </rich-select>
+ */
+
+/* global ObservedPropertiesMixin */
+
+class RichSelect extends ObservedPropertiesMixin(HTMLElement) {
+  static get observedAttributes() {
+    return [
+      "open",
+      "disabled",
+      "hidden",
+    ];
+  }
+
+  constructor() {
+    super();
+
+    this.addEventListener("blur", this);
+    this.addEventListener("click", this);
+    this.addEventListener("keypress", this);
+  }
+
+  connectedCallback() {
+    this.setAttribute("tabindex", "0");
+    this.render();
+  }
+
+  get popupBox() {
+    return this.querySelector(":scope > .rich-select-popup-box");
+  }
+
+  get selectedOption() {
+    return this.popupBox.querySelector(":scope > [selected]");
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "blur": {
+        this.onBlur(event);
+        break;
+      }
+      case "click": {
+        this.onClick(event);
+        break;
+      }
+      case "keypress": {
+        this.onKeyPress(event);
+        break;
+      }
+    }
+  }
+
+  onBlur(event) {
+    if (event.target == this) {
+      this.open = false;
+    }
+  }
+
+  onClick(event) {
+    if (!this.disabled &&
+        event.button == 0) {
+      this.open = !this.open;
+    }
+  }
+
+  onKeyPress(event) {
+    if (event.key == " ") {
+      this.open = !this.open;
+    } else if (event.key == "ArrowDown") {
+      let selectedOption = this.selectedOption;
+      let next = selectedOption.nextElementSibling;
+      if (next) {
+        next.selected = true;
+        selectedOption.selected = false;
+      }
+    } else if (event.key == "ArrowUp") {
+      let selectedOption = this.selectedOption;
+      let next = selectedOption.previousElementSibling;
+      if (next) {
+        next.selected = true;
+        selectedOption.selected = false;
+      }
+    } else if (event.key == "Enter" ||
+               event.key == "Escape") {
+      this.open = false;
+    }
+  }
+
+  _optionsAreEquivalent(a, b) {
+    if (!a || !b) {
+      return false;
+    }
+
+    let aAttrs = a.constructor.observedAttributes;
+    let bAttrs = b.constructor.observedAttributes;
+    if (aAttrs.length != bAttrs.length) {
+      return false;
+    }
+
+    for (let aAttr of aAttrs) {
+      if (a.getAttribute(aAttr) != b.getAttribute(aAttr)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  render() {
+    let popupBox = this.popupBox;
+    if (!popupBox) {
+      popupBox = document.createElement("div");
+      popupBox.classList.add("rich-select-popup-box");
+      this.appendChild(popupBox);
+    }
+
+    /* eslint-disable max-len */
+    let options =
+      this.querySelectorAll(":scope > :not(.rich-select-popup-box):not(.rich-select-selected-clone)");
+    /* eslint-enable max-len */
+    for (let option of options) {
+      popupBox.appendChild(option);
+    }
+
+    let selectedChild;
+    for (let child of popupBox.children) {
+      if (child.selected) {
+        selectedChild = child;
+      }
+    }
+    if (!selectedChild && popupBox.children.length) {
+      selectedChild = popupBox.children[0];
+      selectedChild.selected = true;
+    }
+
+    if (!this._optionsAreEquivalent(this._selectedChild, selectedChild)) {
+      let selectedClone = this.querySelector(":scope > .rich-select-selected-clone");
+      if (selectedClone) {
+        selectedClone.remove();
+      }
+
+      if (selectedChild) {
+        this._selectedChild = selectedChild;
+        selectedClone = selectedChild.cloneNode(false);
+        selectedClone.removeAttribute("id");
+        selectedClone.classList.add("rich-select-selected-clone");
+        selectedClone = this.appendChild(selectedClone);
+      }
+    }
+  }
+}
+
+customElements.define("rich-select", RichSelect);
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -3,25 +3,30 @@
    - 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/. -->
 <!DOCTYPE html>
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <meta http-equiv="Content-Security-Policy" content="default-src 'self'"/>
   <title></title>
   <link rel="stylesheet" href="paymentRequest.css"/>
+  <link rel="stylesheet" href="components/rich-select.css"/>
+  <link rel="stylesheet" href="components/address-option.css"/>
   <script src="vendor/custom-elements.min.js"></script>
 
   <script src="PaymentsStore.js"></script>
 
   <script src="mixins/ObservedPropertiesMixin.js"></script>
   <script src="mixins/PaymentStateSubscriberMixin.js"></script>
 
   <script src="components/currency-amount.js"></script>
 
+  <script src="components/rich-select.js"></script>
+  <script src="components/rich-option.js"></script>
+  <script src="components/address-option.js"></script>
   <script src="containers/payment-dialog.js"></script>
 
   <script src="paymentRequest.js"></script>
 
   <template id="payment-dialog-template">
     <div id="host-name"></div>
 
     <div id="total">
@@ -32,11 +37,55 @@
       <button id="cancel">Cancel</button>
       <button id="pay">Pay</button>
     </div>
   </template>
 </head>
 <body>
   <iframe id="debugging-console" hidden="hidden" src="debugging.html"></iframe>
 
+  <rich-select>
+    <address-option email="emzembrano92@example.com"
+                    recipient="Emily Zembrano"
+                    addressLine="717 Hyde Street #6"
+                    city="San Francisco"
+                    region="CA"
+                    phone="415 203 0845"
+                    postalCode="94109"
+                    country="USA"></address-option>
+    <address-option email="jenz9382@example.com"
+                    recipient="Jennifer Zembrano"
+                    addressLine="42 Fairydust Lane"
+                    city="Lala Land"
+                    region="HI"
+                    phone="415 439 2827"
+                    postalCode="98765"
+                    country="USA"></address-option>
+    <address-option email="johnz9382@example.com"
+                    recipient="John Zembrano"
+                    addressLine="42 Fairydust Lane"
+                    city="Lala Land"
+                    missinginformation="true"
+                    region="HI"
+                    phone="415 439 2827"
+                    postalCode="98765"
+                    country="USA"></address-option>
+    <address-option email="adbrwodne@example.com"
+                    recipient="Andrew Browne"
+                    addressLine="42 Fairydust Lane"
+                    city="Lala Land"
+                    region="HI"
+                    phone="517 410 0845"
+                    postalCode="98765"
+                    country="USA"></address-option>
+    <address-option email="johnz9382@example.com"
+                    recipient="Jacob Humphrey"
+                    addressLine="1855 Pinecrest Rd"
+                    city="East Lansing"
+                    region="MI"
+                    phone="517 439 2827"
+                    postalCode="48823"
+                    country="USA"></address-option>
+  </rich-select>
+
   <payment-dialog></payment-dialog>
 </body>
 </html>
--- a/toolkit/components/payments/test/mochitest/mochitest.ini
+++ b/toolkit/components/payments/test/mochitest/mochitest.ini
@@ -1,14 +1,22 @@
 [DEFAULT]
 support-files =
    ../../../../../testing/modules/sinon-2.3.2.js
    ../../res/PaymentsStore.js
    ../../res/components/currency-amount.js
+   ../../res/components/address-option.js
+   ../../res/components/address-option.css
+   ../../res/components/basic-card-option.js
+   ../../res/components/basic-card-option.css
+   ../../res/components/rich-option.js
+   ../../res/components/rich-select.css
+   ../../res/components/rich-select.js
    ../../res/mixins/ObservedPropertiesMixin.js
    ../../res/mixins/PaymentStateSubscriberMixin.js
    ../../res/vendor/custom-elements.min.js
    ../../res/vendor/custom-elements.min.js.map
    payments_common.js
 
 [test_currency_amount.html]
+[test_rich_select.html]
 [test_ObservedPropertiesMixin.html]
 [test_PaymentStateSubscriberMixin.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/mochitest/test_rich_select.html
@@ -0,0 +1,317 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rich-select component
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the rich-select component</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script src="payments_common.js"></script>
+  <script src="custom-elements.min.js"></script>
+  <script src="ObservedPropertiesMixin.js"></script>
+  <script src="rich-select.js"></script>
+  <script src="rich-option.js"></script>
+  <script src="address-option.js"></script>
+  <script src="basic-card-option.js"></script>
+  <link rel="stylesheet" type="text/css" href="rich-select.css"/>
+  <link rel="stylesheet" type="text/css" href="address-option.css"/>
+  <link rel="stylesheet" type="text/css" href="basic-card-option.css"/>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+    <rich-select id="select1">
+      <address-option id="option1"
+                      email="emzembrano92@email.com"
+                      recipient="Emily Zembrano"
+                      addressLine="717 Hyde Street #6"
+                      city="San Francisco"
+                      region="CA"
+                      phone="415 203 0845"
+                      postalCode="94109"
+                      country="USA"></address-option>
+      <address-option id="option2"
+                      email="jenz9382@email.com"
+                      recipient="Jennifer Zembrano"
+                      addressLine="42 Fairydust Lane"
+                      city="Lala Land"
+                      region="HI"
+                      phone="415 439 2827"
+                      postalCode="98765"
+                      country="USA"></address-option>
+      <address-option id="option3"
+                      email="johnz9382@email.com"
+                      recipient="John Zembrano"
+                      addressLine="42 Fairydust Lane"
+                      city="Lala Land"
+                      missinginformation="true"
+                      region="HI"
+                      phone="415 439 2827"
+                      postalCode="98765"
+                      country="USA"></address-option>
+    </rich-select>
+
+    <rich-select id="select2">
+      <basic-card-option owner="Jared Wein"
+                         expiration="01/1970"
+                         number="4024007197293599"
+                         type="Visa"></basic-card-option>
+      <basic-card-option owner="Whimsy Corn"
+                         expiration="01/1970"
+                         number="5220465104517667"
+                         type="Mastercard"></basic-card-option>
+      <basic-card-option owner="Fire Fox"
+                         expiration="01/1970"
+                         number="6011777095481054"
+                         type="Discover"></basic-card-option>
+    </rich-select>
+  </p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+/** Test the rich-select address-option component **/
+
+/* import-globals-from payments_common.js */
+/* import-globals-from ../../res/components/address-option.js */
+/* import-globals-from ../../res/components/basic-card-option.js */
+
+let select1 = document.getElementById("select1");
+let option1 = document.getElementById("option1");
+let option2 = document.getElementById("option2");
+let option3 = document.getElementById("option3");
+
+function get_selected_clone() {
+  return select1.querySelector(".rich-select-selected-clone");
+}
+
+function is_visible(element, message) {
+  ok(!isHidden(element), message);
+}
+
+function is_hidden(element, message) {
+  ok(isHidden(element), message);
+}
+
+function dispatchKeyPress(key, keyCode) {
+  select1.dispatchEvent(new KeyboardEvent("keypress", {key, keyCode}));
+}
+
+add_task(async function test_addressLine_combines_address_city_region_postalCode_country() {
+  ok(option1, "option1 exists");
+  let addressLine = option1.querySelector(".addressLine");
+  /* eslint-disable max-len */
+  is(addressLine.textContent,
+     `${option1.addressLine} ${option1.city} ${option1.region} ${option1.postalCode} ${option1.country}`);
+  /* eslint-enable max-len */
+});
+
+add_task(async function test_no_option_selected_first_displayed() {
+  ok(select1, "select1 exists");
+
+  await asyncElementRendered();
+
+  is_hidden(option1, "option 1 should be hidden when popup is not open");
+  is_hidden(option2, "option 2 should be hidden when popup is not open");
+  is_hidden(option3, "option 3 should be hidden when popup is not open");
+  ok(option1.selected, "option 1 should be selected");
+  ok(option1.hasAttribute("selected"), "option 1 should have selected attribute");
+  let selectedClone = get_selected_clone();
+  is_visible(selectedClone, "The selected clone should be visible at all times");
+  is(selectedClone.getAttribute("email"), option1.getAttribute("email"),
+     "The selected clone email should be equivalent to the selected option 1");
+  is(selectedClone.getAttribute("recipient"), option1.getAttribute("recipient"),
+     "The selected clone recipient should be equivalent to the selected option 1");
+});
+
+add_task(async function test_clicking_on_select_shows_all_options() {
+  ok(select1, "select1 exists");
+  ok(!select1.open, "select is not open by default");
+  ok(option1.selected, "option 1 should be selected by default");
+
+  select1.click();
+
+  ok(select1.open, "select is open after clicking on it");
+  ok(option1.selected, "option 1 should be selected when open");
+  is_visible(option1, "option 1 is visible when select is open");
+  is_visible(option2, "option 2 is visible when select is open");
+  is_visible(option3, "option 3 is visible when select is open");
+
+  option2.click();
+
+  ok(!select1.open, "select is not open after blur");
+  ok(!option1.selected, "option 1 is not selected after click on option 2");
+  ok(option2.selected, "option 2 is selected after clicking on it");
+  is_hidden(option1, "option 1 is hidden when select is closed");
+  is_hidden(option2, "option 2 is hidden when select is closed");
+  is_hidden(option3, "option 3 is hidden when select is closed");
+
+  await asyncElementRendered();
+
+  let selectedClone = get_selected_clone();
+  is_visible(selectedClone, "The selected clone should be visible at all times");
+  is(selectedClone.getAttribute("email"), option2.getAttribute("email"),
+     "The selected clone email should be equivalent to the selected option 2");
+  is(selectedClone.getAttribute("recipient"), option2.getAttribute("recipient"),
+     "The selected clone recipient should be equivalent to the selected option 2");
+});
+
+add_task(async function test_changing_option_selected_affects_other_options() {
+  ok(option2.selected, "Option 2 should be selected from prior test");
+
+  option1.selected = true;
+  ok(!option2.selected, "Option 2 should no longer be selected after making option 1 selected");
+  ok(option1.hasAttribute("selected"), "Option 1 should now have selected attribute");
+});
+
+add_task(async function test_up_down_keys_change_selected_item() {
+  let openObserver = new MutationObserver(mutations => {
+    for (let mutation of mutations) {
+      ok(mutation.attributeName != "open", "the select should not open/close during this test");
+    }
+  });
+  openObserver.observe(select1, {attributes: true});
+
+  ok(select1, "select1 exists");
+  ok(option1.selected, "option 1 should be selected by default");
+
+  ok(!select1.open, "select should not be open before focusing");
+  select1.focus();
+  ok(!select1.open, "select should not be open after focusing");
+
+  dispatchKeyPress("ArrowDown", 40);
+  ok(!option1.selected, "option 1 should no longer be selected");
+  ok(option2.selected, "option 2 should now be selected");
+
+  dispatchKeyPress("ArrowDown", 40);
+  ok(!option2.selected, "option 2 should no longer be selected");
+  ok(option3.selected, "option 3 should now be selected");
+
+  dispatchKeyPress("ArrowDown", 40);
+  ok(option3.selected, "option 3 should remain selected");
+  ok(!option1.selected, "option 1 should not be selected");
+
+  dispatchKeyPress("ArrowUp", 38);
+  ok(!option3.selected, "option 3 should no longer be selected");
+  ok(option2.selected, "option 2 should now be selected");
+
+  dispatchKeyPress("ArrowUp", 38);
+  ok(!option2.selected, "option 2 should no longer be selected");
+  ok(option1.selected, "option 1 should now be selected");
+
+  dispatchKeyPress("ArrowUp", 38);
+  ok(option1.selected, "option 1 should remain selected");
+  ok(!option3.selected, "option 3 should not be selected");
+
+  // Wait for any mutation observer notifications to fire before exiting.
+  await Promise.resolve();
+
+  openObserver.disconnect();
+});
+
+add_task(async function test_open_close_from_keyboard() {
+  select1.focus();
+
+  ok(!select1.open, "select should not be open by default");
+
+  dispatchKeyPress(" ", 32);
+  ok(select1.open, "select should now be open");
+  ok(option1.selected, "option 1 should be selected by default");
+
+  dispatchKeyPress("ArrowDown", 40);
+  ok(!option1.selected, "option 1 should not be selected");
+  ok(option2.selected, "option 2 should now be selected");
+  ok(select1.open, "select should remain open");
+
+  dispatchKeyPress("ArrowUp", 38);
+  ok(option1.selected, "option 1 should now be selected");
+  ok(!option2.selected, "option 2 should not be selected");
+  ok(select1.open, "select should remain open");
+
+  dispatchKeyPress("Enter", 13);
+  ok(option1.selected, "option 1 should now be selected");
+  ok(!select1.open, "select should be closed");
+
+  dispatchKeyPress(" ", 32);
+  ok(select1.open, "select should now be open");
+
+  dispatchKeyPress("Escape", 27);
+  ok(!select1.open, "select should be closed");
+});
+
+add_task(async function test_clicking_on_options_maintain_one_item_always_selected() {
+  ok(!select1.open, "select should be closed by default");
+  ok(option1.selected, "option 1 should be selected by default");
+  select1.click();
+  ok(select1.open, "select should now be open");
+
+  option3.click();
+  ok(!select1.open, "select should be closed");
+  ok(!option1.selected, "option 1 should be unselected");
+  ok(option3.selected, "option 3 should be selected");
+
+  select1.click();
+  ok(select1.open, "select should open");
+  ok(!option1.selected, "option 1 should be unselected");
+  ok(option3.selected, "option 3 should be selected");
+
+  option1.click();
+  ok(!select1.open, "select should be closed");
+  ok(option1.selected, "option 1 should be selected");
+  ok(!option3.selected, "option 3 should be unselected");
+});
+
+add_task(async function test_selected_clone_should_equal_selected_option() {
+  ok(option1.selected, "option 1 should be selected");
+  await asyncElementRendered();
+
+  let clonedOptions = select1.querySelectorAll(".rich-select-selected-clone");
+  is(clonedOptions.length, 1, "there should only be one cloned option");
+
+  let clonedOption = clonedOptions[0];
+  for (let attrName of AddressOption.observedAttributes) {
+    is(clonedOption.attributes[attrName] && clonedOption.attributes[attrName].value,
+       option1.attributes[attrName] && option1.attributes[attrName].value,
+       "attributes should have matching value; name=" + attrName);
+  }
+
+  option2.selected = true;
+  await asyncElementRendered();
+
+  clonedOptions = select1.querySelectorAll(".rich-select-selected-clone");
+  is(clonedOptions.length, 1, "there should only be one cloned option");
+
+  clonedOption = clonedOptions[0];
+  for (let attrName of AddressOption.observedAttributes) {
+    is(clonedOption.attributes[attrName] && clonedOption.attributes[attrName].value,
+       option2.attributes[attrName] && option2.attributes[attrName].value,
+       "attributes should have matching value; name=" + attrName);
+  }
+});
+
+add_task(async function test_basic_card_simple() {
+  let select2 = document.getElementById("select2");
+  ok(select2, "basic card select should exist");
+  let selectPopupBox = select2.querySelector(".rich-select-popup-box");
+  ok(selectPopupBox, "basic card popup box exists");
+
+  is(selectPopupBox.childElementCount, 3, "There should be three children in the popup box");
+
+  let clonedOption = select2.querySelector(".rich-select-selected-clone");
+  let selectedOption = selectPopupBox.firstChild;
+  for (let attrName of BasicCardOption.observedAttributes) {
+    is(clonedOption.attributes[attrName] && clonedOption.attributes[attrName].value,
+       selectedOption.attributes[attrName] && selectedOption.attributes[attrName].value,
+       "attributes should have matching value; name=" + attrName);
+  }
+});
+
+</script>
+
+</body>
+</html>