Merge autoland to mozilla-central. a=merge
authorNarcis Beleuzu <nbeleuzu@mozilla.com>
Thu, 21 Jun 2018 12:46:57 +0300
changeset 809154 e834d23a292972ab4250a8be00e6740c43e41db2
parent 809124 a79434a2511b80f92554c15317e2d6271849ace9 (current diff)
parent 809153 a0c124cdf70fae2889fa93ac3c0d08f4a0b4fe68 (diff)
child 809155 b41c6dc5ad624e77a37965d178ad02c61834593b
child 809163 507ba4664baa33a608b6756058415e457df6ad8e
child 809168 f8f265b3f58174c7219ec148853865f3013372b9
child 809187 50ea4de900701dc7c65334a8798d1f4a3b23dcb5
child 809201 6238846cd1001bdb312d3dea1b16817c850ded6d
child 809204 5c7a126e8ec4fa72296fefc1eb8d17ebdd29086b
child 809209 5c2fa592dafaa290ac0bfb8b702b164c2bdb723f
child 809210 51201fa1b709b84f643c5916797e23f41c75b4f9
child 809212 97f3563466e000152ee32dadc3b7f096c97aed27
child 809218 3748fca7402005b52262a141e75091f3daca7b2b
child 809219 8b0191d46711ac4f11ed62d6c45d45283351abbb
child 809231 30a6d566972724d1523fc4a076ee022d66a9346e
child 809241 f5a2237b579fe3c4a91b9e097bd2bda3caa90500
child 809259 2e2d20a5defc6ef6ffe8d3109fa306a154249c13
child 809260 1c33a38da75d550750923358e73d7af2102b9c1d
child 809281 f1b748497b0f1928041fe8e99668d66c87b0f3b2
child 809343 776ba1aa0feee265e8b7ee7a570bac7114433a9a
child 809518 7a291166d62f696241e6a7c935457e2b53e48b17
child 809628 48daa11f847cefda10088e80a7bdf4c22805f1d7
child 810073 96bcfc40c1a66c556786e64113ae984095d55769
child 810110 e7cc0308bb09aa28aafb9940bdc71e34ef9b1a23
child 810116 1f2512383904022f6e1856db723827b3cf1942cf
child 810132 0eca88ae74b06148e38cab712a678ca2d86db462
child 810158 070d33dd4d436d04665d6f883b5e4e2587e52ad3
child 810780 f9bad1673d18903a0a4dcc180fbb8b789d8a95cb
child 811462 7156bb6c9049eef36499b2cb34915f58f3f6e02a
child 811792 1b5e862e324a4f7f17d606061d3c649e7ec2008f
child 812498 398538bd1c97f96f72bbbbe647a9c2ec6195877b
child 819375 6a71b2d44ae208139ba147bc4e302abe76589fe7
push id113554
push userbmo:daniel@haxx.se
push dateThu, 21 Jun 2018 10:18:22 +0000
reviewersmerge
milestone62.0a1
Merge autoland to mozilla-central. a=merge
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -113,18 +113,19 @@ export default class BasicCardForm exten
 
     this.cancelButton.textContent = this.dataset.cancelButtonLabel;
     this.backButton.textContent = this.dataset.backButtonLabel;
     this.saveButton.textContent = this.dataset.saveButtonLabel;
     this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
     this.addressAddLink.textContent = this.dataset.addressAddLinkLabel;
     this.addressEditLink.textContent = this.dataset.addressEditLinkLabel;
 
-    // The back button is temporarily hidden(See Bug 1462461).
-    this.backButton.hidden = !!page.onboardingWizard;
+    // The next line needs an onboarding check since we don't set previousId
+    // when navigating to add/edit directly from the summary page.
+    this.backButton.hidden = !page.previousId && page.onboardingWizard;
     this.cancelButton.hidden = !page.onboardingWizard;
 
     let record = {};
     let basicCards = paymentRequest.getBasicCards(state);
     let addresses = paymentRequest.getAddresses(state);
 
     this.genericErrorText.textContent = page.error;
 
@@ -205,21 +206,49 @@ export default class BasicCardForm exten
         if (evt.target == this.addressEditLink && selectedOption && selectedOption.value) {
           nextState["address-page"].title = this.dataset.billingAddressTitleEdit;
           nextState["address-page"].guid = selectedOption.value;
         }
         this.requestStore.setState(nextState);
         break;
       }
       case this.backButton: {
-        this.requestStore.setState({
+        let {
+          page,
+          request,
+          "address-page": addressPage,
+          "basic-card-page": basicCardPage,
+          selectedShippingAddress,
+        } = this.requestStore.getState();
+
+        let nextState = {
           page: {
-            id: "payment-summary",
+            id: page.previousId || "payment-summary",
+            onboardingWizard: page.onboardingWizard,
           },
-        });
+        };
+
+        let addressPageState;
+        if (page.onboardingWizard) {
+          if (request.paymentOptions.requestShipping) {
+            addressPageState = Object.assign({}, addressPage, {guid: selectedShippingAddress});
+          } else {
+            addressPageState =
+              Object.assign({}, addressPage, {guid: basicCardPage.billingAddressGUID});
+          }
+
+          let basicCardPageState = Object.assign({}, basicCardPage, {preserveFieldValues: true});
+
+          Object.assign(nextState, {
+            "address-page": addressPageState,
+            "basic-card-page": basicCardPageState,
+          });
+        }
+
+        this.requestStore.setState(nextState);
         break;
       }
       case this.saveButton: {
         this.saveRecord();
         break;
       }
       default: {
         throw new Error("Unexpected click target");
--- a/browser/components/payments/test/browser/browser_payments_onboarding_wizard.js
+++ b/browser/components/payments/test/browser/browser_payments_onboarding_wizard.js
@@ -401,8 +401,103 @@ add_task(async function test_on_boarding
 
     info("Closing the payment dialog");
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
 
     cleanupFormAutofillStorage();
   });
 });
+
+add_task(async function test_back_button_on_basic_card_page_during_onboarding() {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: BLANK_PAGE_URL,
+  }, async browser => {
+    cleanupFormAutofillStorage();
+
+    info("Opening the payment dialog");
+    let {win, frame} =
+        await setupPaymentDialog(browser, {
+          methodData: [PTU.MethodData.basicCard],
+          details: PTU.Details.total60USD,
+          merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+        });
+
+    await spawnPaymentDialogTask(frame, async function() {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+      await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "address-page";
+      }, "Address page is shown first if there are saved addresses during on boarding");
+
+      info("Checking if the address page has been rendered");
+      let addressSaveButton = content.document.querySelector("address-form .save-button");
+      ok(content.isVisible(addressSaveButton), "Address save button is rendered");
+
+      for (let [key, val] of Object.entries(PTU.Addresses.TimBL2)) {
+        let field = content.document.getElementById(key);
+        if (!field) {
+          ok(false, `${key} field not found`);
+        }
+        field.value = val;
+        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+      }
+      content.document.querySelector("address-form .save-button").click();
+
+      await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "basic-card-page";
+      }, "Basic card page is shown next");
+
+      info("Checking if basic card page is rendered");
+      let basicCardBackButton = content.document.querySelector("basic-card-form .back-button");
+      ok(content.isVisible(basicCardBackButton), "Back button is visible on the basic card page");
+
+      info("Partially fill basic card form");
+      let field = content.document.getElementById("cc-number");
+      field.value = PTU.BasicCards.JohnDoe["cc-number"];
+
+      info("Clicking on the back button to edit address saved in the previous step");
+      basicCardBackButton.click();
+
+      await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "address-page" &&
+               state["address-page"].guid == state["basic-card-page"].billingAddressGUID;
+      }, "Address page is shown again");
+
+      info("Checking if the address page has been rendered");
+      addressSaveButton = content.document.querySelector("address-form .save-button");
+      ok(content.isVisible(addressSaveButton), "Address save button is rendered");
+
+      info("Checking if the address saved in the last step is correctly loaded in the form");
+      field = content.document.getElementById("given-name");
+      ok(field.value, PTU.Addresses.TimBL2["given-name"],
+         "Given name field value is correctly loaded");
+
+      info("Editing the address and saving again");
+      field.value = "John";
+      addressSaveButton.click();
+
+      info("Checking if the address was correctly edited");
+      await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "basic-card-page" &&
+               // eslint-disable-next-line max-len
+               state.savedAddresses[state["basic-card-page"].billingAddressGUID]["given-name"] == "John";
+      }, "Address was correctly edited and saved");
+
+      // eslint-disable-next-line max-len
+      info("Checking if the basic card form is now rendered and if the field values from before are preserved");
+      let basicCardCancelButton = content.document.querySelector("basic-card-form .cancel-button");
+      ok(content.isVisible(basicCardCancelButton),
+         "Cancel button is visible on the basic card page");
+      field = content.document.getElementById("cc-number");
+      ok(field.value, PTU.BasicCards.JohnDoe["cc-number"], "Values in the form are preserved");
+    });
+
+    info("Closing the payment dialog");
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+
+    cleanupFormAutofillStorage();
+  });
+});
--- a/devtools/client/inspector/animation/components/AnimationItem.js
+++ b/devtools/client/inspector/animation/components/AnimationItem.js
@@ -29,35 +29,37 @@ class AnimationItem extends Component {
       timeScale: PropTypes.object.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
-      isSelected: false,
+      isSelected: this.isSelected(props),
     };
   }
 
   componentWillReceiveProps(nextProps) {
-    const { animation } = this.props;
-
     this.setState({
-      isSelected: nextProps.selectedAnimation &&
-                  animation.actorID === nextProps.selectedAnimation.actorID
+      isSelected: this.isSelected(nextProps),
     });
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     return this.state.isSelected !== nextState.isSelected ||
            this.props.animation !== nextProps.animation ||
            this.props.timeScale !== nextProps.timeScale;
   }
 
+  isSelected(props) {
+    return props.selectedAnimation &&
+           props.animation.actorID === props.selectedAnimation.actorID;
+  }
+
   render() {
     const {
       animation,
       emitEventForTest,
       getAnimatedPropertyMap,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
--- a/devtools/client/inspector/animation/reducers/animations.js
+++ b/devtools/client/inspector/animation/reducers/animations.js
@@ -43,18 +43,22 @@ const reducers = {
       animations,
       detailVisibility,
       selectedAnimation,
       timeScale: new TimeScale(animations),
     });
   },
 
   [UPDATE_DETAIL_VISIBILITY](state, { detailVisibility }) {
+    const selectedAnimation =
+      detailVisibility ? state.selectedAnimation : null;
+
     return Object.assign({}, state, {
-      detailVisibility
+      detailVisibility,
+      selectedAnimation,
     });
   },
 
   [UPDATE_ELEMENT_PICKER_ENABLED](state, { elementPickerEnabled }) {
     return Object.assign({}, state, {
       elementPickerEnabled
     });
   },
--- a/devtools/client/inspector/animation/test/browser.ini
+++ b/devtools/client/inspector/animation/test/browser.ini
@@ -21,16 +21,18 @@ support-files =
 
 [browser_animation_animated-property-list.js]
 [browser_animation_animated-property-list_unchanged-items.js]
 [browser_animation_animated-property-name.js]
 [browser_animation_animation-detail_close-button.js]
 [browser_animation_animation-detail_title.js]
 [browser_animation_animation-detail_visibility.js]
 [browser_animation_animation-list.js]
+[browser_animation_animation-list_one-animation-select.js]
+[browser_animation_animation-list_select.js]
 [browser_animation_animation-target.js]
 [browser_animation_animation-target_highlight.js]
 [browser_animation_animation-target_select.js]
 [browser_animation_animation-timeline-tick.js]
 [browser_animation_css-transition-with-playstate-idle.js]
 [browser_animation_current-time-label.js]
 [browser_animation_current-time-scrubber.js]
 [browser_animation_current-time-scrubber_each-different-creation-time-animations.js]
--- a/devtools/client/inspector/animation/test/browser_animation_animated-property-list.js
+++ b/devtools/client/inspector/animation/test/browser_animation_animated-property-list.js
@@ -33,22 +33,10 @@ add_task(async function() {
                                            panel, `.${ targetClass }`);
     ok(panel.querySelector(".animated-property-list"),
       `The animated-property-list should be in the DOM at ${ targetClass }`);
     const itemEls =
       panel.querySelectorAll(".animated-property-list .animated-property-item");
     is(itemEls.length, expectedNumber,
        `The number of animated-property-list should be ${ expectedNumber } ` +
        `at ${ targetClass }`);
-
-    if (itemEls.length < 2) {
-      continue;
-    }
-
-    info("Checking the background color for " +
-         `the animated property item at ${ targetClass }`);
-    const evenColor = panel.ownerGlobal.getComputedStyle(itemEls[0]).backgroundColor;
-    const oddColor = panel.ownerGlobal.getComputedStyle(itemEls[1]).backgroundColor;
-    isnot(evenColor, oddColor,
-          "Background color of an even animated property item " +
-          "should be different from odd");
   }
 });
--- a/devtools/client/inspector/animation/test/browser_animation_animation-list.js
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-list.js
@@ -12,22 +12,13 @@ add_task(async function() {
 
   info("Checking animation list and items existence");
   ok(panel.querySelector(".animation-list"),
     "The animation-list is in the DOM");
   is(panel.querySelectorAll(".animation-list .animation-item").length,
      animationInspector.state.animations.length,
      "The number of animations displayed matches the number of animations");
 
-  info("Checking the background color for the animation list items");
-  const animationItemEls = panel.querySelectorAll(".animation-list .animation-item");
-  const evenColor =
-    panel.ownerGlobal.getComputedStyle(animationItemEls[0]).backgroundColor;
-  const oddColor =
-    panel.ownerGlobal.getComputedStyle(animationItemEls[1]).backgroundColor;
-  isnot(evenColor, oddColor,
-    "Background color of an even animation should be different from odd");
-
   info("Checking list and items existence after select a element which has an animation");
   await selectNodeAndWaitForAnimations(".animated", inspector);
   is(panel.querySelectorAll(".animation-list .animation-item").length, 1,
     "The number of animations displayed should be 1 for .animated element");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-list_one-animation-select.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the animation item has been selected from first time
+// if count of the animations is one.
+
+add_task(async function() {
+  await addTab(URL_ROOT + "doc_simple_animation.html");
+  await removeAnimatedElementsExcept([".animated"]);
+  const { panel } = await openAnimationInspector();
+
+  info("Checking whether an item element has been selected");
+  is(panel.querySelector(".animation-item").classList.contains("selected"), true,
+     "The animation item should have 'selected' class");
+
+  info("Checking whether the element will be unselected after closing the detail pane");
+  clickOnDetailCloseButton(panel);
+  is(panel.querySelector(".animation-item").classList.contains("selected"), false,
+     "The animation item should not have 'selected' class");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-list_select.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the animation items in the list were selectable.
+
+add_task(async function() {
+  await addTab(URL_ROOT + "doc_simple_animation.html");
+  await removeAnimatedElementsExcept([".animated", ".long"]);
+  const { animationInspector, panel } = await openAnimationInspector();
+
+  info("Checking whether 1st element will be selected");
+  await clickOnAnimation(animationInspector, panel, 0);
+  assertSelection(panel, [true, false]);
+
+  info("Checking whether 2nd element will be selected");
+  await clickOnAnimation(animationInspector, panel, 1);
+  assertSelection(panel, [false, true]);
+
+  info("Checking whether all elements will be unselected after closing the detail pane");
+  clickOnDetailCloseButton(panel);
+  assertSelection(panel, [false, false]);
+});
+
+function assertSelection(panel, expectedResult) {
+  panel.querySelectorAll(".animation-item").forEach((item, index) => {
+    const shouldSelected = expectedResult[index];
+    is(item.classList.contains("selected"), shouldSelected,
+       `Animation item[${ index }] should ` +
+       `${ shouldSelected ? "" : "not" } have 'selected' class`);
+  });
+}
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -1,40 +1,42 @@
 /* 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/. */
 
 /* Animation-inspector specific theme variables */
 
 :root {
-  --animation-even-background-color: rgba(0, 0, 0, 0.05);
+  --animation-item-hover-color: var(--grey-30-a40);
+  --animation-item-selected-color: var(--grey-30-a90);
   --command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
+  --cssanimation-color: var(--purple-50);
+  --csstransition-color: var(--blue-55);
   --devtools-toolbar-height: 24px;
   --fast-track-image: url("images/animation-fast-track.svg");
-  --fill-color-cssanimation: var(--theme-contrast-background);
-  --fill-color-csstransition: var(--theme-highlight-blue);
-  --fill-color-scriptanimation: var(--theme-graphs-green);
   --graph-height: 30px;
   --graph-right-offset: 10px;
   --keyframe-marker-shadow-color: #c4c4c4;
   --pause-image: url(chrome://devtools/skin/images/pause.svg);
-  --progress-bar-color: #909090;
+  --progress-bar-color: var(--grey-40);
   --resume-image: url(chrome://devtools/skin/images/play.svg);
   --rewind-image: url(chrome://devtools/skin/images/rewind.svg);
-  --scrubber-color: #dd00a9;
+  --scriptanimation-color: var(--green-60);
+  --scrubber-color: var(--magenta-65);
   --sidebar-width: 200px;
-  --stroke-color-cssanimation: var(--theme-highlight-lightorange);
-  --stroke-color-csstransition: var(--theme-highlight-bluegrey);
-  --stroke-color-scriptanimation: var(--theme-highlight-green);
-  --tick-line-style: 0.5px solid rgba(128, 136, 144, 0.5);
+  --tick-line-style: 0.5px solid var(--theme-splitter-color);
 }
 
 :root.theme-dark {
-  --animation-even-background-color: rgba(255, 255, 255, 0.05);
+  --animation-item-hover-color: var(--grey-60-a50);
+  --animation-item-selected-color: var(--grey-60);
+  --csstransition-color: var(--blue-50);
   --keyframe-marker-shadow-color: #818181;
+  --progress-bar-color: var(--grey-50);
+  --scrubber-color: var(--magenta-50);
 }
 
 /* Root element of animation inspector */
 #animation-container {
   cursor: default;
   display: flex;
   flex-direction: column;
   height: 100%;
@@ -127,37 +129,37 @@ select.playback-rate-selector.devtools-b
 }
 
 .indication-bar.current-time-scrubber::after {
   border-left-color: var(--scrubber-color);
   left: 5px;
 }
 
 /* Animation Item */
-.animation-item:nth-child(2n+1) {
-  background-color: var(--animation-even-background-color);
-}
-
 .animation-item.cssanimation {
-  --computed-timing-graph-color: var(--fill-color-cssanimation);
-  --effect-timing-graph-color: var(--stroke-color-cssanimation);
+  --graph-color: var(--cssanimation-color);
+  --graph-opacity: 0.7;
 }
 
 .animation-item.csstransition {
-  --computed-timing-graph-color: var(--fill-color-csstransition);
-  --effect-timing-graph-color: var(--stroke-color-csstransition);
+  --graph-color: var(--csstransition-color);
+  --graph-opacity: 0.8;
 }
 
 .animation-item.scriptanimation {
-  --computed-timing-graph-color: var(--fill-color-scriptanimation);
-  --effect-timing-graph-color: var(--stroke-color-scriptanimation);
+  --graph-color: var(--scriptanimation-color);
+  --graph-opacity: 0.5;
+}
+
+.animation-item:hover {
+  background-color: var(--animation-item-hover-color);
 }
 
 .animation-item.selected {
-  background-color: var(--theme-selection-background-hover);
+  background-color: var(--animation-item-selected-color);
 }
 
 /* Animation Target */
 .animation-target {
   align-items: center;
   display: flex;
   grid-column: 1 / 2;
   height: var(--graph-height);
@@ -186,17 +188,18 @@ select.playback-rate-selector.devtools-b
   background-color: var(--theme-highlight-blue);
 }
 
 /* Summary Graph */
 .animation-summary-graph {
   cursor: pointer;
   grid-column: 2 / 3;
   height: var(--graph-height);
-  padding-top: 5px;
+  padding-bottom: 3px;
+  padding-top: 3px;
   position: relative;
 }
 
 .animation-summary-graph.compositor::after {
   background-image: var(--fast-track-image);
   background-repeat: no-repeat;
   content: "";
   display: block;
@@ -210,28 +213,29 @@ select.playback-rate-selector.devtools-b
 }
 
 .animation-summary-graph-path {
   height: 100%;
   width: 100%;
 }
 
 .animation-computed-timing-path path {
-  fill: var(--computed-timing-graph-color);
+  fill: var(--graph-color);
+  fill-opacity: var(--graph-opacity);
   vector-effect: non-scaling-stroke;
   transform: scale(1, -1);
 }
 
 .animation-computed-timing-path path.infinity:nth-child(n+2) {
   opacity: 0.3;
 }
 
 .animation-effect-timing-path path {
   fill: none;
-  stroke: var(--effect-timing-graph-color);
+  stroke: var(--graph-color);
   stroke-dasharray: 2px 2px;
   transform: scale(1, -1);
   vector-effect: non-scaling-stroke;
 }
 
 .animation-effect-timing-path path.infinity:nth-child(n+2) {
   opacity: 0.3;
 }
@@ -245,33 +249,33 @@ select.playback-rate-selector.devtools-b
   vector-effect: non-scaling-stroke;
 }
 
 .animation-delay-sign,
 .animation-end-delay-sign {
   background-color: var(--theme-graphs-grey);
   height: 3px;
   position: absolute;
-  top: calc(100% - 1.5px);
+  bottom: 2px;
 }
 
 .animation-delay-sign::before,
 .animation-end-delay-sign::before {
   background-color: inherit;
   border-radius: 50%;
   content: "";
   height: 6px;
   position: absolute;
   top: -1.5px;
   width: 6px;
 }
 
 .animation-delay-sign.fill,
 .animation-end-delay-sign.fill {
-  background-color: var(--effect-timing-graph-color);
+  background-color: var(--graph-color);
 }
 
 .animation-delay-sign.negative::before {
   left: unset;
   right: -3px;
 }
 
 .animation-end-delay-sign::before {
@@ -300,17 +304,17 @@ select.playback-rate-selector.devtools-b
   stroke-linejoin: round;
   stroke-opacity: .5;
   stroke-width: 4;
   text-anchor: end;
 }
 
 /* Animation Detail */
 .animation-detail-container {
-  background-color: var(--theme-body-background);
+  background-color: var(--theme-sidebar-background);
   display: flex;
   flex-direction: column;
   height: 100%;
   overflow: hidden;
   width: 100%;
   z-index: 2;
 }
 
@@ -360,20 +364,16 @@ select.playback-rate-selector.devtools-b
   border-top-color: var(--progress-bar-color);
 }
 
 .indication-bar.keyframes-progress-bar::after {
   border-left-color: var(--progress-bar-color);
 }
 
 /* Animated Property Item */
-.animated-property-item:nth-child(2n+1) {
-  background-color: var(--animation-even-background-color);
-}
-
 .animated-property-item.unchanged {
   opacity: 0.6;
 }
 
 /* Animated Property Name */
 .animated-property-name {
   align-items: center;
   display: flex;
@@ -383,25 +383,25 @@ select.playback-rate-selector.devtools-b
 }
 
 .animated-property-name.compositor span {
   padding-left: 15px;
   position: relative;
 }
 
 .animated-property-list-container.cssanimation .animated-property-name.compositor {
-  --fast-track-color: var(--stroke-color-cssanimation);
+  --fast-track-color: var(--cssanimation-color);
 }
 
 .animated-property-list-container.csstransition .animated-property-name.compositor {
-  --fast-track-color: var(--stroke-color-csstransition);
+  --fast-track-color: var(--csstransition-color);
 }
 
 .animated-property-list-container.scriptanimation .animated-property-name.compositor {
-  --fast-track-color: var(--stroke-color-scriptanimation);
+  --fast-track-color: var(--scriptanimation-color);
 }
 
 .animated-property-name.compositor span::before {
   background-image: var(--fast-track-image);
   background-repeat: no-repeat;
   background-size: contain;
   content: "";
   fill: var(--fast-track-color);
@@ -425,30 +425,31 @@ select.playback-rate-selector.devtools-b
 }
 
 .keyframes-graph-path {
   height: 100%;
   width: 100%;
 }
 
 .keyframes-graph-path path {
-  fill: #00b0bd88;
-  stroke: #00b0bd;
+  fill: var(--teal-60);
+  fill-opacity: 0.5;
+  stroke: var(--teal-70);
   vector-effect: non-scaling-stroke;
   transform: scale(1, -1);
 }
 
 .keyframes-graph.opacity .keyframes-graph-path path {
-  fill: #df00a988;
-  stroke: #df00a9;
+  fill: var(--magenta-50);
+  stroke: var(--magenta-70);
 }
 
 .keyframes-graph.transform .keyframes-graph-path path {
-  fill: #ea800088;
-  stroke: #ea8000;
+  fill: var(--yellow-50);
+  stroke: var(--yellow-60);
 }
 
 .keyframes-graph-path .color-path path {
   stroke: none;
 }
 
 .keyframes-graph .keyframes-graph-path .hint path {
   fill: none;
@@ -491,25 +492,25 @@ select.playback-rate-selector.devtools-b
   position: absolute;
   top: 50%;
   height: 10px;
   transform: translate(-5px, -3px);
   width: 10px;
 }
 
 .animated-property-list-container.cssanimation .keyframe-marker-item {
-  background-color: var(--fill-color-cssanimation);
+  background-color: var(--cssanimation-color);
 }
 
 .animated-property-list-container.csstransition .keyframe-marker-item {
-  background-color: var(--fill-color-csstransition);
+  background-color: var(--csstransition-color);
 }
 
 .animated-property-list-container.scriptanimation .keyframe-marker-item {
-  background-color: var(--fill-color-scriptanimation);
+  background-color: var(--scriptanimation-color);
 }
 
 /* Common Components */
 /* Progress Inspection Panel */
 .progress-inspection-panel {
   height: 100%;
   overflow-y: auto;
   overflow-x: hidden;
--- a/devtools/client/themes/variables.css
+++ b/devtools/client/themes/variables.css
@@ -208,38 +208,49 @@
   --toolbarbutton-checked-border-color: var(--toolbarbutton-border-color);
   --toolbarbutton-checked-focus-background: var(--blue-60);
 
   /* The photon animation curve */
   --animation-curve: cubic-bezier(.07,.95,0,1);
 
   /* Firefox Colors CSS Variables v1.0.3
    * Colors are taken from: https://github.com/FirefoxUX/design-tokens */
+  --magenta-50: #ff1ad9;
   --magenta-65: #dd00a9;
+  --magenta-70: #b5007f;
 
+  --purple-50: #9400ff;
   --purple-60: #8000d7;
 
   --blue-40: #45a1ff;
   --blue-50: #0a84ff;
   --blue-55: #0074e8;
   --blue-60: #0060df;
   --blue-70: #003eaa;
 
+  --teal-60: #00c8d7;
+  --teal-70: #008ea4;
+
   --red-70: #a4000f;
 
   --green-50: #30e60b;
   --green-60: #12bc00;
   --green-70: #058b00;
 
+  --yellow-50: #ffe900;
+  --yellow-60: #d7b600;
   --yellow-80: #715100;
 
   --grey-10: #f9f9fa;
   --grey-20: #ededf0;
   --grey-30: #d7d7db;
+  --grey-30-a40: rgba(215, 215, 219, 0.4);
+  --grey-30-a90: rgba(215, 215, 219, 0.9);
   --grey-40: #b1b1b3;
   --grey-50: #737373;
   --grey-60: #4a4a4f;
+  --grey-60-a50: rgba(74, 74, 79, 0.5);
   --grey-70: #38383d;
   --grey-80: #2a2a2e;
   --grey-90: #0c0c0d;
   --grey-90-a10: rgba(12, 12, 13, 0.1);
   --grey-90-a80: rgba(12, 12, 13, 0.8);
 }
--- a/dom/payments/PaymentRequest.cpp
+++ b/dom/payments/PaymentRequest.cpp
@@ -644,33 +644,31 @@ already_AddRefed<Promise>
 PaymentRequest::CanMakePayment(ErrorResult& aRv)
 {
   if (mState != eCreated) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
   if (mResultPromise) {
+    // XXX This doesn't match the spec but does match Chromium.
     aRv.Throw(NS_ERROR_DOM_NOT_ALLOWED_ERR);
     return nullptr;
   }
 
-  nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
+  nsIGlobalObject* global = GetOwnerGlobal();
   ErrorResult result;
   RefPtr<Promise> promise = Promise::Create(global, result);
   if (result.Failed()) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
-  if (NS_WARN_IF(!manager)) {
-    aRv.Throw(NS_ERROR_FAILURE);
-    return nullptr;
-  }
+  MOZ_ASSERT(manager);
   nsresult rv = manager->CanMakePayment(this);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     promise->MaybeReject(NS_ERROR_FAILURE);
     return promise.forget();
   }
   mResultPromise = promise;
   return promise.forget();
 }
@@ -682,48 +680,51 @@ PaymentRequest::RespondCanMakePayment(bo
   mResultPromise->MaybeResolve(aResult);
   mResultPromise = nullptr;
 }
 
 already_AddRefed<Promise>
 PaymentRequest::Show(const Optional<OwningNonNull<Promise>>& aDetailsPromise,
                      ErrorResult& aRv)
 {
-  if (mState != eCreated) {
-    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
-    return nullptr;
-  }
-
   if (!EventStateManager::IsHandlingUserInput()) {
     aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return nullptr;
   }
 
-  nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
+  nsIGlobalObject* global = GetOwnerGlobal();
+  nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(global);
+  MOZ_ASSERT(win);
+  nsIDocument* doc = win->GetExtantDoc();
+  if (!doc || !doc->IsCurrentActiveDocument()) {
+    aRv.Throw(NS_ERROR_DOM_ABORT_ERR);
+    return nullptr;
+  }
+
+  if (mState != eCreated) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return nullptr;
+  }
+
   ErrorResult result;
   RefPtr<Promise> promise = Promise::Create(global, result);
   if (result.Failed()) {
     mState = eClosed;
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
-  RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
-  if (NS_WARN_IF(!manager)) {
-    mState = eClosed;
-    aRv.Throw(NS_ERROR_FAILURE);
-    return nullptr;
-  }
-
   if (aDetailsPromise.WasPassed()) {
     aDetailsPromise.Value().AppendNativeHandler(this);
     mUpdating = true;
     mDeferredShow = true;
   }
 
+  RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
+  MOZ_ASSERT(manager);
   nsresult rv = manager->ShowPayment(this);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     if (rv == NS_ERROR_ABORT) {
       promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
     } else {
       promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
     }
     mState = eClosed;
@@ -789,35 +790,31 @@ already_AddRefed<Promise>
 PaymentRequest::Abort(ErrorResult& aRv)
 {
   if (mState != eInteractive) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
   if (mAbortPromise) {
-    aRv.Throw(NS_ERROR_DOM_NOT_ALLOWED_ERR);
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
-  nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
+  nsIGlobalObject* global = GetOwnerGlobal();
   ErrorResult result;
   RefPtr<Promise> promise = Promise::Create(global, result);
   if (result.Failed()) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
-  if (NS_WARN_IF(!manager)) {
-    aRv.Throw(NS_ERROR_FAILURE);
-    return nullptr;
-  }
-
-  // It's possible for to call this between show and its promise resolving.
+  MOZ_ASSERT(manager);
+  // It's possible to be called between show and its promise resolving.
   nsresult rv = manager->AbortPayment(this, mDeferredShow);
   mDeferredShow = false;
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   mAbortPromise = promise;
@@ -1055,17 +1052,19 @@ PaymentRequest::RejectedCallback(JSConte
   mUpdating = false;
   AbortUpdate(NS_ERROR_DOM_ABORT_ERR, mDeferredShow);
   mDeferredShow = false;
 }
 
 PaymentRequest::~PaymentRequest()
 {
   if (mIPC) {
-    mIPC->MaybeDelete();
+    // If we're being destroyed, the PaymentRequestManager isn't holding any
+    // references to us and we can't be waiting for any replies.
+    mIPC->MaybeDelete(false);
   }
 }
 
 JSObject*
 PaymentRequest::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return PaymentRequestBinding::Wrap(aCx, this, aGivenProto);
 }
--- a/dom/payments/PaymentRequestManager.cpp
+++ b/dom/payments/PaymentRequestManager.cpp
@@ -304,16 +304,27 @@ PaymentRequestManager::NotifyRequestDone
   MOZ_ASSERT(entry.Data() > 0);
 
   uint32_t count = --entry.Data();
   if (count == 0) {
     entry.Remove();
   }
 }
 
+void
+PaymentRequestManager::RequestIPCOver(PaymentRequest* aRequest)
+{
+  // This must only be called from ActorDestroy or if we're sure we won't
+  // receive any more IPC for aRequest.
+  mActivePayments.Remove(aRequest);
+  if (aRequest == mShowingRequest) {
+    mShowingRequest = nullptr;
+  }
+}
+
 already_AddRefed<PaymentRequestManager>
 PaymentRequestManager::GetSingleton()
 {
   if (!gPaymentManager) {
     gPaymentManager = new PaymentRequestManager();
     ClearOnShutdown(&gPaymentManager);
   }
   RefPtr<PaymentRequestManager> manager = gPaymentManager;
--- a/dom/payments/PaymentRequestManager.h
+++ b/dom/payments/PaymentRequestManager.h
@@ -59,16 +59,20 @@ public:
 
   nsresult RespondPayment(PaymentRequest* aRequest,
                           const IPCPaymentActionResponse& aResponse);
   nsresult ChangeShippingAddress(PaymentRequest* aRequest,
                                  const IPCPaymentAddress& aAddress);
   nsresult ChangeShippingOption(PaymentRequest* aRequest,
                                 const nsAString& aOption);
 
+  // Called to ensure that we don't "leak" aRequest if we shut down while it had
+  // an active request to the parent.
+  void RequestIPCOver(PaymentRequest* aRequest);
+
 private:
   PaymentRequestManager() = default;
   ~PaymentRequestManager()
   {
     MOZ_ASSERT(mActivePayments.Count() == 0);
   }
 
   PaymentRequestChild* GetPaymentChild(PaymentRequest* aRequest);
--- a/dom/payments/ipc/PaymentRequestChild.cpp
+++ b/dom/payments/ipc/PaymentRequestChild.cpp
@@ -81,43 +81,40 @@ PaymentRequestChild::RecvChangeShippingO
   }
   return IPC_OK();
 }
 
 void
 PaymentRequestChild::ActorDestroy(ActorDestroyReason aWhy)
 {
   if (mRequest) {
-    DetachFromRequest();
+    DetachFromRequest(true);
   }
 }
 
 void
-PaymentRequestChild::MaybeDelete()
+PaymentRequestChild::MaybeDelete(bool aCanBeInManager)
 {
   if (mRequest) {
-    DetachFromRequest();
+    DetachFromRequest(aCanBeInManager);
     Send__delete__(this);
   }
 }
 
-bool
-PaymentRequestChild::SendRequestPayment(const IPCPaymentActionRequest& aAction)
-{
-  return PPaymentRequestChild::SendRequestPayment(aAction);
-}
-
 void
-PaymentRequestChild::DetachFromRequest()
+PaymentRequestChild::DetachFromRequest(bool aCanBeInManager)
 {
   MOZ_ASSERT(mRequest);
-  nsAutoString id;
-  mRequest->GetInternalId(id);
 
-  RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
-  MOZ_ASSERT(manager);
+  if (aCanBeInManager) {
+    RefPtr<PaymentRequestManager> manager = PaymentRequestManager::GetSingleton();
+    MOZ_ASSERT(manager);
+
+    RefPtr<PaymentRequest> request(mRequest);
+    manager->RequestIPCOver(request);
+  }
 
   mRequest->SetIPC(nullptr);
   mRequest = nullptr;
 }
 
 } // end of namespace dom
 } // end of namespace mozilla
--- a/dom/payments/ipc/PaymentRequestChild.h
+++ b/dom/payments/ipc/PaymentRequestChild.h
@@ -14,17 +14,17 @@ namespace dom {
 
 class PaymentRequest;
 
 class PaymentRequestChild final : public PPaymentRequestChild
 {
 public:
   explicit PaymentRequestChild(PaymentRequest* aRequest);
 
-  void MaybeDelete();
+  void MaybeDelete(bool aCanBeInManager);
 
   nsresult RequestPayment(const IPCPaymentActionRequest& aAction);
 
 protected:
   mozilla::ipc::IPCResult
   RecvRespondPayment(const IPCPaymentActionResponse& aResponse) override;
 
   mozilla::ipc::IPCResult
@@ -35,17 +35,17 @@ protected:
   RecvChangeShippingOption(const nsString& aRequestId,
                            const nsString& aOption) override;
 
   void ActorDestroy(ActorDestroyReason aWhy) override;
 
 private:
   ~PaymentRequestChild() = default;
 
-  bool SendRequestPayment(const IPCPaymentActionRequest& aAction);
-  void DetachFromRequest();
+  void DetachFromRequest(bool aCanBeInManager);
+
   PaymentRequest* MOZ_NON_OWNING_REF mRequest;
 };
 
 } // end of namespace dom
 } // end of namespace mozilla
 
 #endif
--- a/dom/vr/VRDisplay.cpp
+++ b/dom/vr/VRDisplay.cpp
@@ -258,27 +258,25 @@ VRDisplayCapabilities::WrapObject(JSCont
 {
   return VRDisplayCapabilitiesBinding::Wrap(aCx, this, aGivenProto);
 }
 
 VRPose::VRPose(nsISupports* aParent, const gfx::VRHMDSensorState& aState)
   : Pose(aParent)
   , mVRState(aState)
 {
-  mFrameId = aState.inputFrameID;
   mozilla::HoldJSObjects(this);
 }
 
 VRPose::VRPose(nsISupports* aParent)
   : Pose(aParent)
 {
   mVRState.inputFrameID = 0;
   mVRState.timestamp = 0.0;
   mVRState.flags = gfx::VRDisplayCapabilityFlags::Cap_None;
-  mFrameId = 0;
   mozilla::HoldJSObjects(this);
 }
 
 VRPose::~VRPose()
 {
   mozilla::DropJSObjects(this);
 }
 
--- a/dom/vr/VRDisplay.h
+++ b/dom/vr/VRDisplay.h
@@ -98,18 +98,16 @@ protected:
 
 class VRPose final : public Pose
 {
 
 public:
   VRPose(nsISupports* aParent, const gfx::VRHMDSensorState& aState);
   explicit VRPose(nsISupports* aParent);
 
-  uint64_t FrameID() const { return mFrameId; }
-
   virtual void GetPosition(JSContext* aCx,
                            JS::MutableHandle<JSObject*> aRetval,
                            ErrorResult& aRv) override;
   virtual void GetLinearVelocity(JSContext* aCx,
                                  JS::MutableHandle<JSObject*> aRetval,
                                  ErrorResult& aRv) override;
   virtual void GetLinearAcceleration(JSContext* aCx,
                                      JS::MutableHandle<JSObject*> aRetval,
@@ -124,17 +122,16 @@ public:
                                       JS::MutableHandle<JSObject*> aRetval,
                                       ErrorResult& aRv) override;
 
   virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
 protected:
   ~VRPose();
 
-  uint64_t mFrameId;
   gfx::VRHMDSensorState mVRState;
 };
 
 struct VRFrameInfo
 {
   VRFrameInfo();
 
   void Update(const gfx::VRDisplayInfo& aInfo,
--- a/taskcluster/mach_commands.py
+++ b/taskcluster/mach_commands.py
@@ -267,53 +267,45 @@ class MachCommands(MachCommandBase):
                      help='Action input (.yml or .json)')
     @CommandArgument('--task', default=None,
                      help='Task definition (.yml or .json; if omitted, the task will be'
                           'fetched from the queue)')
     @CommandArgument('callback', default=None,
                      help='Action callback name (Python function name)')
     def test_action_callback(self, **options):
         import taskgraph.parameters
-        from taskgraph.util.taskcluster import get_task_definition
         import taskgraph.actions
         import yaml
 
         def load_data(filename):
             with open(filename) as f:
                 if filename.endswith('.yml'):
                     return yaml.safe_load(f)
                 elif filename.endswith('.json'):
                     return json.load(f)
                 else:
                     raise Exception("unknown filename {}".format(filename))
 
         try:
             self.setup_logging()
             task_id = options['task_id']
-            if options['task']:
-                task = load_data(options['task'])
-            elif task_id:
-                task = get_task_definition(task_id)
-            else:
-                task = None
 
             if options['input']:
                 input = load_data(options['input'])
             else:
                 input = None
 
             parameters = taskgraph.parameters.load_parameters_file(options['parameters'])
             parameters.check()
 
             root = options['root']
 
             return taskgraph.actions.trigger_action_callback(
                     task_group_id=options['task_group_id'],
                     task_id=task_id,
-                    task=task,
                     input=input,
                     callback=options['callback'],
                     parameters=parameters,
                     root=root,
                     test=True)
         except Exception:
             traceback.print_exc()
             sys.exit(1)
--- a/taskcluster/taskgraph/actions/backfill.py
+++ b/taskcluster/taskgraph/actions/backfill.py
@@ -39,27 +39,45 @@ logger = logging.getLogger(__name__)
             'depth': {
                 'type': 'integer',
                 'default': 5,
                 'minimum': 1,
                 'maximum': 10,
                 'title': 'Depth',
                 'description': ('The number of previous pushes before the current '
                                 'push to attempt to trigger this task on.')
+            },
+            'inclusive': {
+                'type': 'boolean',
+                'default': False,
+                'title': 'Inclusive Range',
+                'description': ('If true, the backfill will also retrigger the task '
+                                'on the selected push.')
+            },
+            'addGeckoProfile': {
+                'type': 'boolean',
+                'default': False,
+                'title': 'Add Gecko Profile',
+                'description': 'If true, appends --geckoProfile to mozharness options.'
+            },
+            'testPath': {
+                'type': 'string',
+                'title': 'Test Path',
             }
         },
         'additionalProperties': False
     },
     available=lambda parameters: parameters.get('project', None) != 'try'
 )
 def backfill_action(parameters, graph_config, input, task_group_id, task_id, task):
     label = task['metadata']['name']
     pushes = []
-    depth = input.get('depth', 5)
-    end_id = int(parameters['pushlog_id']) - 1
+    inclusive_tweak = 1 if input.get('inclusive') else 0
+    depth = input.get('depth', 5) + inclusive_tweak
+    end_id = int(parameters['pushlog_id']) - (1 - inclusive_tweak)
 
     while True:
         start_id = max(end_id - depth, 0)
         pushlog_url = PUSHLOG_TMPL.format(parameters['head_repository'], start_id, end_id)
         r = requests.get(pushlog_url)
         r.raise_for_status()
         pushes = pushes + r.json()['pushes'].keys()
         if len(pushes) >= depth:
@@ -85,13 +103,27 @@ def backfill_action(parameters, graph_co
                     INDEX_TMPL.format(parameters['project'], push),
                     'public/parameters.yml')
             push_decision_task_id = find_decision_task(push_params, graph_config)
         except HTTPError as e:
             logger.info('Skipping {} due to missing index artifacts! Error: {}'.format(push, e))
             continue
 
         if label in full_task_graph.tasks.keys():
-            create_tasks(
-                    [label], full_task_graph, label_to_taskid,
-                    push_params, push_decision_task_id, push)
+            def modifier(task):
+                if task.label != label:
+                    return task
+                if input.get('addGeckoProfile'):
+                    mh = task.task['payload'].setdefault('env', {}) \
+                                                    .get('MOZHARNESS_OPTIONS', '')
+                    task.task['payload']['env']['MOZHARNESS_OPTIONS'] = mh + ' --geckoProfile'
+                    task.task['extra']['treeherder']['symbol'] += '-p'
+
+                if input.get('testPath'):
+                    env = task.task['payload'].setdefault('env', {})
+                    env['MOZHARNESS_TEST_PATHS'] = input.get('testPath')
+                    task.task['extra']['treeherder']['symbol'] += '-b'
+                return task
+
+            create_tasks([label], full_task_graph, label_to_taskid,
+                         push_params, push_decision_task_id, push, modifier=modifier)
         else:
             logging.info('Could not find {} on {}. Skipping.'.format(label, push))
--- a/taskcluster/taskgraph/actions/util.py
+++ b/taskcluster/taskgraph/actions/util.py
@@ -106,35 +106,40 @@ def create_task_from_def(task_id, task_d
 
 
 def update_parent(task, graph):
     task.task.setdefault('extra', {})['parent'] = os.environ.get('TASK_ID', '')
     return task
 
 
 def create_tasks(to_run, full_task_graph, label_to_taskid,
-                 params, decision_task_id=None, suffix=''):
+                 params, decision_task_id=None, suffix='', modifier=lambda t: t):
     """Create new tasks.  The task definition will have {relative-datestamp':
     '..'} rendered just like in a decision task.  Action callbacks should use
     this function to create new tasks,
     allowing easy debugging with `mach taskgraph action-callback --test`.
     This builds up all required tasks to run in order to run the tasks requested.
 
+    Optionally this function takes a `modifier` function that is passed in each
+    task before it is put into a new graph. It should return a valid task. Note
+    that this is passed _all_ tasks in the graph, not just the set in to_run. You
+    may want to skip modifying tasks not in your to_run list.
+
     If you wish to create the tasks in a new group, leave out decision_task_id."""
     if suffix != '':
         suffix = '-{}'.format(suffix)
     to_run = set(to_run)
 
     #  Copy to avoid side-effects later
     full_task_graph = copy.deepcopy(full_task_graph)
     label_to_taskid = label_to_taskid.copy()
 
     target_graph = full_task_graph.graph.transitive_closure(to_run)
     target_task_graph = TaskGraph(
-        {l: full_task_graph[l] for l in target_graph.nodes},
+        {l: modifier(full_task_graph[l]) for l in target_graph.nodes},
         target_graph)
     target_task_graph.for_each_task(update_parent)
     optimized_task_graph, label_to_taskid = optimize_task_graph(target_task_graph,
                                                                 params,
                                                                 to_run,
                                                                 label_to_taskid)
     write_artifact('task-graph{}.json'.format(suffix), optimized_task_graph.to_json())
     write_artifact('label-to-taskid{}.json'.format(suffix), label_to_taskid)
--- a/taskcluster/taskgraph/task.py
+++ b/taskcluster/taskgraph/task.py
@@ -16,17 +16,17 @@ class Task(object):
     - optimization: optimization to apply to the task (see taskgraph.optimize)
     - dependencies: tasks this one depends on, in the form {name: label}, for example
       {'build': 'build-linux64/opt', 'docker-image': 'build-docker-image-desktop-test'}
 
     And later, as the task-graph processing proceeds:
 
     - task_id -- TaskCluster taskId under which this task will be created
 
-    This class is just a convenience wraper for the data type and managing
+    This class is just a convenience wrapper for the data type and managing
     display, comparison, serialization, etc. It has no functionality of its own.
     """
     def __init__(self, kind, label, attributes, task,
                  optimization=None, dependencies=None):
         self.kind = kind
         self.label = label
         self.attributes = attributes
         self.task = task
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -597458,17 +597458,17 @@
    "2280f0ef821cdc3093e10c2162d3756f5eeb78de",
    "testharness"
   ],
   "payment-request/payment-request-abort-method.https.html": [
    "a9d811dc41487ba7a8e5d55319574364b93362aa",
    "testharness"
   ],
   "payment-request/payment-request-canmakepayment-method-manual.https.html": [
-   "7531ea3b11f6e8de8cf71666b19861dbc5267cf7",
+   "11df3310832e043e533a11245b012b9de1c0bb26",
    "manual"
   ],
   "payment-request/payment-request-constructor-crash.https.html": [
    "383d1c3f9505ee63d504bee87e13efa90ba49f3d",
    "testharness"
   ],
   "payment-request/payment-request-constructor.https.html": [
    "511c4d25939d344f187fe4a8daa4a8ff926b613d",
--- a/testing/web-platform/tests/payment-request/payment-request-canmakepayment-method-manual.https.html
+++ b/testing/web-platform/tests/payment-request/payment-request-canmakepayment-method-manual.https.html
@@ -121,19 +121,22 @@ promise_test(async t => {
       );
       break;
     }
   }
 }, `Optionally, at the user agent's discretion, return a promise rejected with a "NotAllowedError" DOMException.`);
 
 function manualTest1(elem){
   elem.disabled = true;
+
+  // NB: request.show has to be called outside of promise_test to ensure the
+  // user's click is still visible to PaymentRequest.show.
+  const request = new PaymentRequest(defaultMethods, defaultDetails);
+  const acceptPromise = request.show(); // Sets state to "interactive"
   promise_test(async t => {
-    const request = new PaymentRequest(defaultMethods, defaultDetails);
-    const acceptPromise = request.show(); // Sets state to "interactive"
     const canMakePaymentPromise = request.canMakePayment();
     try {
       const result = await canMakePaymentPromise;
       assert_true(
         false,
         `canMakePaymentPromise should have thrown InvalidStateError`
       );
     } catch (err) {
@@ -143,21 +146,24 @@ function manualTest1(elem){
       await promise_rejects(t, "AbortError", acceptPromise);
     }
     // The state should be "closed"
     await promise_rejects(t, "InvalidStateError", request.canMakePayment());
   }, elem.textContent.trim());
 }
 
 function manualTest2(elem){
- elem.disabled = true;
+  elem.disabled = true;
+
+  // See above for why it's important for these lines to be outside of
+  // promise_test.
+  const request = new PaymentRequest(defaultMethods, defaultDetails);
+  const acceptPromise = request.show(); // The state is now "interactive"
+  acceptPromise.catch(() => {}); // no-op, just to silence unhandled rejection in devtools.
   promise_test(async t => {
-    const request = new PaymentRequest(defaultMethods, defaultDetails);
-    const acceptPromise = request.show(); // The state is now "interactive"
-    acceptPromise.catch(() => {}); // no-op, just to silence unhandled rejection in devtools.
     await request.abort(); // The state is now "closed"
     await promise_rejects(t, "InvalidStateError", request.canMakePayment());
     try {
       const result = await request.canMakePayment();
       assert_true(
         false,
         `should have thrown InvalidStateError, but instead returned "${result}"`
       );
--- a/toolkit/library/moz.build
+++ b/toolkit/library/moz.build
@@ -19,16 +19,18 @@ def Libxul(name):
         GeckoFramework(name, linkage=None)
         SHARED_LIBRARY_NAME = 'XUL'
     else:
         GeckoSharedLibrary(name, linkage=None)
         SHARED_LIBRARY_NAME = 'xul'
 
     DELAYLOAD_DLLS += [
         'comdlg32.dll',
+        'hid.dll',
+        'msimg32.dll',
         'netapi32.dll',
         'secur32.dll',
         'wininet.dll',
         'winspool.drv'
     ]
 
     if CONFIG['ACCESSIBILITY']:
         DELAYLOAD_DLLS += ['oleacc.dll']