Merge mozilla-central to autoland. a=merge CLOSED TREE
authorshindli <shindli@mozilla.com>
Thu, 17 Jan 2019 11:58:24 +0200
changeset 511356 ae73df344b934c73241e61809a8d42835c33952b
parent 511355 bb0949deaa67c9e36d8a931aba8dd7f2cbf067c0 (current diff)
parent 511333 24f969298ec5c1a7cbc444bfc8f3d77b07f46bbe (diff)
child 511357 177103817494df65f31adc379f46af1dac849d10
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone66.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
Merge mozilla-central to autoland. a=merge CLOSED TREE
toolkit/mozapps/update/tests/data/update.sjs
--- a/devtools/client/inspector/rules/components/Declaration.js
+++ b/devtools/client/inspector/rules/components/Declaration.js
@@ -9,38 +9,46 @@ const dom = require("devtools/client/sha
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const Types = require("../types");
 
 class Declaration extends PureComponent {
   static get propTypes() {
     return {
       declaration: PropTypes.shape(Types.declaration).isRequired,
+      onToggleDeclaration: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
       // Whether or not the computed property list is expanded.
       isComputedListExpanded: false,
     };
 
     this.onComputedExpanderClick = this.onComputedExpanderClick.bind(this);
+    this.onToggleDeclarationClick = this.onToggleDeclarationClick.bind(this);
   }
 
   onComputedExpanderClick(event) {
     event.stopPropagation();
 
     this.setState(prevState => {
       return { isComputedListExpanded: !prevState.isComputedListExpanded };
     });
   }
 
+  onToggleDeclarationClick(event) {
+    event.stopPropagation();
+    const { id, ruleId } = this.props.declaration;
+    this.props.onToggleDeclaration(ruleId, id);
+  }
+
   renderComputedPropertyList() {
     const { computedProperties } = this.props.declaration;
 
     if (!computedProperties.length) {
       return null;
     }
 
     return (
@@ -126,16 +134,17 @@ class Declaration extends PureComponent 
           className: "ruleview-property" +
                      (!isEnabled || !isKnownProperty || isOverridden ?
                       " ruleview-overridden" : ""),
         },
         dom.div({ className: "ruleview-propertycontainer" },
           dom.div({
             className: "ruleview-enableproperty theme-checkbox" +
                         (isEnabled ? " checked" : ""),
+            onClick: this.onToggleDeclarationClick,
             tabIndex: -1,
           }),
           dom.span({ className: "ruleview-namecontainer" },
             dom.span({ className: "ruleview-propertyname theme-fg-color5" }, name),
             ": "
           ),
           dom.span({
             className: "ruleview-expander theme-twisty" +
--- a/devtools/client/inspector/rules/components/Declarations.js
+++ b/devtools/client/inspector/rules/components/Declarations.js
@@ -11,32 +11,37 @@ const PropTypes = require("devtools/clie
 const Declaration = createFactory(require("./Declaration"));
 
 const Types = require("../types");
 
 class Declarations extends PureComponent {
   static get propTypes() {
     return {
       declarations: PropTypes.arrayOf(PropTypes.shape(Types.declaration)).isRequired,
+      onToggleDeclaration: PropTypes.func.isRequired,
     };
   }
 
   render() {
-    const { declarations } = this.props;
+    const {
+      declarations,
+      onToggleDeclaration,
+    } = this.props;
 
     if (!declarations.length) {
       return null;
     }
 
     return (
       dom.ul({ className: "ruleview-propertylist" },
         declarations.map(declaration => {
           return Declaration({
             key: declaration.id,
             declaration,
+            onToggleDeclaration,
           });
         })
       )
     );
   }
 }
 
 module.exports = Declarations;
--- a/devtools/client/inspector/rules/components/Rule.js
+++ b/devtools/client/inspector/rules/components/Rule.js
@@ -12,22 +12,26 @@ const Declarations = createFactory(requi
 const Selector = createFactory(require("./Selector"));
 const SourceLink = createFactory(require("./SourceLink"));
 
 const Types = require("../types");
 
 class Rule extends PureComponent {
   static get propTypes() {
     return {
+      onToggleDeclaration: PropTypes.func.isRequired,
       rule: PropTypes.shape(Types.rule).isRequired,
     };
   }
 
   render() {
-    const { rule } = this.props;
+    const {
+      onToggleDeclaration,
+      rule,
+    } = this.props;
     const {
       declarations,
       selector,
       sourceLink,
       type,
     } = rule;
 
     return (
@@ -37,17 +41,20 @@ class Rule extends PureComponent {
         dom.div({ className: "ruleview-code" },
           dom.div({},
             Selector({
               selector,
               type,
             }),
             dom.span({ className: "ruleview-ruleopen" }, " {")
           ),
-          Declarations({ declarations }),
+          Declarations({
+            declarations,
+            onToggleDeclaration,
+          }),
           dom.div({ className: "ruleview-ruleclose" }, "}")
         )
       )
     );
   }
 }
 
 module.exports = Rule;
--- a/devtools/client/inspector/rules/components/Rules.js
+++ b/devtools/client/inspector/rules/components/Rules.js
@@ -9,23 +9,30 @@ const PropTypes = require("devtools/clie
 
 const Rule = createFactory(require("./Rule"));
 
 const Types = require("../types");
 
 class Rules extends PureComponent {
   static get propTypes() {
     return {
+      onToggleDeclaration: PropTypes.func.isRequired,
       rules: PropTypes.arrayOf(PropTypes.shape(Types.rule)).isRequired,
     };
   }
 
   render() {
-    return this.props.rules.map(rule => {
+    const {
+      onToggleDeclaration,
+      rules,
+    } = this.props;
+
+    return rules.map(rule => {
       return Rule({
         key: rule.id,
+        onToggleDeclaration,
         rule,
       });
     });
   }
 }
 
 module.exports = Rules;
--- a/devtools/client/inspector/rules/components/RulesApp.js
+++ b/devtools/client/inspector/rules/components/RulesApp.js
@@ -23,16 +23,17 @@ const Toolbar = createFactory(require(".
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
 
 const SHOW_PSEUDO_ELEMENTS_PREF = "devtools.inspector.show_pseudo_elements";
 
 class RulesApp extends PureComponent {
   static get propTypes() {
     return {
+      onToggleDeclaration: PropTypes.func.isRequired,
       onTogglePseudoClass: PropTypes.func.isRequired,
       rules: PropTypes.arrayOf(PropTypes.shape(Types.rule)).isRequired,
     };
   }
 
   renderInheritedRules(rules) {
     if (!rules.length) {
       return null;
@@ -45,17 +46,20 @@ class RulesApp extends PureComponent {
       if (rule.inheritance.inherited !== lastInherited) {
         lastInherited = rule.inheritance.inherited;
 
         output.push(
           dom.div({ className: "ruleview-header" }, rule.inheritance.inheritedSource)
         );
       }
 
-      output.push(Rule({ rule }));
+      output.push(Rule({
+        onToggleDeclaration: this.props.onToggleDeclaration,
+        rule,
+      }));
     }
 
     return output;
   }
 
   renderKeyframesRules(rules) {
     if (!rules.length) {
       return null;
@@ -70,16 +74,17 @@ class RulesApp extends PureComponent {
       }
 
       lastKeyframes = rule.keyframesRule.id;
 
       const items = [
         {
           component: Rules,
           componentProps: {
+            onToggleDeclaration: this.props.onToggleDeclaration,
             rules: rules.filter(r => r.keyframesRule.id === lastKeyframes),
           },
           header: rule.keyframesRule.keyframesName,
           opened: true,
         },
       ];
 
       output.push(Accordion({ items }));
@@ -88,28 +93,34 @@ class RulesApp extends PureComponent {
     return output;
   }
 
   renderStyleRules(rules) {
     if (!rules.length) {
       return null;
     }
 
-    return Rules({ rules });
+    return Rules({
+      onToggleDeclaration: this.props.onToggleDeclaration,
+      rules,
+    });
   }
 
   renderPseudoElementRules(rules) {
     if (!rules.length) {
       return null;
     }
 
     const items = [
       {
         component: Rules,
-        componentProps: { rules },
+        componentProps: {
+          onToggleDeclaration: this.props.onToggleDeclaration,
+          rules,
+        },
         header: getStr("rule.pseudoElement"),
         opened: Services.prefs.getBoolPref(SHOW_PSEUDO_ELEMENTS_PREF),
         onToggled: () => {
           const opened = Services.prefs.getBoolPref(SHOW_PSEUDO_ELEMENTS_PREF);
           Services.prefs.setBoolPref(SHOW_PSEUDO_ELEMENTS_PREF, !opened);
         },
       },
     ];
--- a/devtools/client/inspector/rules/models/element-style.js
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -125,16 +125,27 @@ ElementStyle.prototype = {
       }
       return promiseWarn(e);
     });
     this.populated = populated;
     return this.populated;
   },
 
   /**
+   * Returns the Rule object of the given rule id.
+   *
+   * @param  {String} id
+   *         The id of the Rule object.
+   * @return {Rule|undefined} of the given rule id or undefined if it cannot be found.
+   */
+  getRule: function(id) {
+    return this.rules.find(rule => rule.domRule.actorID === id);
+  },
+
+  /**
    * Get the font families in use by the element.
    *
    * Returns a promise that will be resolved to a list of CSS family
    * names. The list might have duplicates.
    */
   getUsedFontFamilies: function() {
     return new Promise((resolve, reject) => {
       this.ruleView.styleWindow.requestIdleCallback(async () => {
@@ -317,16 +328,38 @@ ElementStyle.prototype = {
       // overridden state has changed for the text property.
       if (this._updatePropertyOverridden(textProp)) {
         textProp.updateEditor();
       }
     }
   },
 
   /**
+   * Toggles the enabled state of the given CSS declaration.
+   *
+   * @param {String} ruleId
+   *        The Rule id of the given CSS declaration.
+   * @param {String} declarationId
+   *        The TextProperty id for the CSS declaration.
+   */
+  toggleDeclaration: function(ruleId, declarationId) {
+    const rule = this.getRule(ruleId);
+    if (!rule) {
+      return;
+    }
+
+    const declaration = rule.getDeclaration(declarationId);
+    if (!declaration) {
+      return;
+    }
+
+    declaration.setEnabled(!declaration.enabled);
+  },
+
+  /**
    * Mark a given TextProperty as overridden or not depending on the
    * state of its computed properties. Clears the _overriddenDirty state
    * on all computed properties.
    *
    * @param  {TextProperty} prop
    *         The text property to update.
    * @return {Boolean} true if the TextProperty's overridden state (or any of
    *         its computed properties overridden state) changed.
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -164,16 +164,28 @@ Rule.prototype = {
   /**
    * The rule's column within a stylesheet
    */
   get ruleColumn() {
     return this.domRule ? this.domRule.column : null;
   },
 
   /**
+   * Returns the TextProperty with the given id or undefined if it cannot be found.
+   *
+   * @param {String} id
+   *        A TextProperty id.
+   * @return {TextProperty|undefined} with the given id in the current Rule or undefined
+   * if it cannot be found.
+   */
+  getDeclaration: function(id) {
+    return this.textProps.find(textProp => textProp.id === id);
+  },
+
+  /**
    * Returns true if the rule matches the creation options
    * specified.
    *
    * @param {Object} options
    *        Creation options. See the Rule constructor for documentation.
    */
   matches: function(options) {
     return this.domRule === options.rule;
--- a/devtools/client/inspector/rules/models/text-property.js
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -1,16 +1,18 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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 { generateUUID } = require("devtools/shared/generate-uuid");
+
 loader.lazyRequireGetter(this, "escapeCSSComment", "devtools/shared/css/parsing-utils", true);
 
 /**
  * TextProperty is responsible for the following:
  *   Manages a single property from the authoredText attribute of the
  *     relevant declaration.
  *   Maintains a list of computed properties that come from this
  *     property declaration.
@@ -30,16 +32,17 @@ loader.lazyRequireGetter(this, "escapeCS
  * @param {Boolean} invisible
  *        Whether the property is invisible.  An invisible property
  *        does not show up in the UI; these are needed so that the
  *        index of a property in Rule.textProps is the same as the index
  *        coming from parseDeclarations.
  */
 function TextProperty(rule, name, value, priority, enabled = true,
                       invisible = false) {
+  this.id = name + "_" + generateUUID().toString();
   this.rule = rule;
   this.name = name;
   this.value = value;
   this.priority = priority;
   this.enabled = !!enabled;
   this.invisible = invisible;
   this.cssProperties = this.rule.elementStyle.ruleView.cssProperties;
   this.panelDoc = this.rule.elementStyle.ruleView.inspector.panelDoc;
--- a/devtools/client/inspector/rules/new-rules.js
+++ b/devtools/client/inspector/rules/new-rules.js
@@ -27,36 +27,40 @@ const PREF_UA_STYLES = "devtools.inspect
 class RulesView {
   constructor(inspector, window) {
     this.cssProperties = inspector.cssProperties;
     this.doc = window.document;
     this.inspector = inspector;
     this.pageStyle = inspector.pageStyle;
     this.selection = inspector.selection;
     this.store = inspector.store;
+    this.telemetry = inspector.telemetry;
     this.toolbox = inspector.toolbox;
 
     this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
 
     this.onSelection = this.onSelection.bind(this);
+    this.onToggleDeclaration = this.onToggleDeclaration.bind(this);
     this.onTogglePseudoClass = this.onTogglePseudoClass.bind(this);
+    this.updateRules = this.updateRules.bind(this);
 
     this.inspector.sidebar.on("select", this.onSelection);
     this.selection.on("detached-front", this.onSelection);
     this.selection.on("new-node-front", this.onSelection);
 
     this.init();
   }
 
   init() {
     if (!this.inspector) {
       return;
     }
 
     const rulesApp = RulesApp({
+      onToggleDeclaration: this.onToggleDeclaration,
       onTogglePseudoClass: this.onTogglePseudoClass,
     });
 
     const provider = createElement(Provider, {
       id: "ruleview",
       key: "ruleview",
       store: this.store,
       title: INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
@@ -79,16 +83,17 @@ class RulesView {
     this.cssProperties = null;
     this.doc = null;
     this.elementStyle = null;
     this.inspector = null;
     this.pageStyle = null;
     this.selection = null;
     this.showUserAgentStyles = null;
     this.store = null;
+    this.telemetry = null;
     this.toolbox = null;
   }
 
   /**
    * Creates a dummy element in the document that helps get the computed style in
    * TextProperty.
    *
    * @return {Element} used to get the computed style for text properties.
@@ -128,16 +133,31 @@ class RulesView {
       this.update();
       return;
     }
 
     this.update(this.selection.nodeFront);
   }
 
   /**
+   * Handler for toggling the enabled property for a given CSS declaration.
+   *
+   * @param  {String} ruleId
+   *         The Rule id of the given CSS declaration.
+   * @param  {String} declarationId
+   *         The TextProperty id for the CSS declaration.
+   */
+  onToggleDeclaration(ruleId, declarationId) {
+    this.elementStyle.toggleDeclaration(ruleId, declarationId);
+    this.telemetry.recordEvent("edit_rule", "ruleview", null, {
+      "session_id": this.toolbox.sessionId,
+    });
+  }
+
+  /**
    * Handler for toggling a pseudo class in the pseudo class panel. Toggles on and off
    * a given pseudo class value.
    *
    * @param  {String} value
    *         The pseudo class to toggle on or off.
    */
   onTogglePseudoClass(value) {
     this.store.dispatch(togglePseudoClass(value));
@@ -156,16 +176,25 @@ class RulesView {
     if (!element) {
       this.store.dispatch(disableAllPseudoClasses());
       this.store.dispatch(updateRules([]));
       return;
     }
 
     this.elementStyle = new ElementStyle(element, this, {}, this.pageStyle,
       this.showUserAgentStyles);
+    this.elementStyle.onChanged = this.updateRules;
     await this.elementStyle.populate();
 
     this.store.dispatch(setPseudoClassLocks(this.elementStyle.element.pseudoClassLocks));
+    this.updateRules();
+  }
+
+  /**
+   * Updates the rules view by dispatching the current rules state. This is called from
+   * the update() function, and from the ElementStyle's onChange() handler.
+   */
+  updateRules() {
     this.store.dispatch(updateRules(this.elementStyle.rules));
   }
 }
 
 module.exports = RulesView;
--- a/devtools/client/inspector/rules/reducers/rules.js
+++ b/devtools/client/inspector/rules/reducers/rules.js
@@ -14,53 +14,55 @@ const INITIAL_RULES = {
 };
 
 /**
  * Given a rule's TextProperty, returns the properties that are needed to render a
  * CSS declaration.
  *
  * @param  {TextProperty} declaration
  *         A TextProperty of a rule.
- * @param  {Number} index
- *         The index of the CSS declaration within the declaration block.
+ * @param  {String} ruleId
+ *         The rule id that is associated with the given CSS declaration.
  * @return {Object} containing the properties needed to render a CSS declaration.
  */
-function getDeclarationState(declaration, index) {
+function getDeclarationState(declaration, ruleId) {
   return {
     // Array of the computed properties for a CSS declaration.
     computedProperties: declaration.computedProperties,
     // An unique CSS declaration id.
-    id: `${declaration.name}${declaration.value}${index}`,
+    id: declaration.id,
     // Whether or not the declaration is enabled.
     isEnabled: declaration.enabled,
     // Whether or not the declaration's property name is known.
     isKnownProperty: declaration.isKnownProperty,
     // Whether or not the the declaration is overridden.
     isOverridden: !!declaration.overridden,
     // The declaration's property name.
     name: declaration.name,
     // The declaration's priority (either "important" or an empty string).
     priority: declaration.priority,
+    // The CSS rule id that is associated with this CSS declaration.
+    ruleId,
     // The declaration's property value.
     value: declaration.value,
   };
 }
 
 /**
  * Given a Rule, returns the properties that are needed to render a CSS rule.
  *
  * @param  {Rule} rule
  *         A Rule object containing information about a CSS rule.
  * @return {Object} containing the properties needed to render a CSS rule.
  */
 function getRuleState(rule) {
   return {
     // Array of CSS declarations.
-    declarations: rule.declarations.map((declaration, i) =>
-      getDeclarationState(declaration, i)),
+    declarations: rule.declarations.map(declaration =>
+      getDeclarationState(declaration, rule.domRule.actorID)),
     // An unique CSS rule id.
     id: rule.domRule.actorID,
     // An object containing information about the CSS rule's inheritance.
     inheritance: rule.inheritance,
     // Whether or not the rule does not match the current selected element.
     isUnmatched: rule.isUnmatched,
     // Whether or not the rule is an user agent style.
     isUserAgentStyle: rule.isSystem,
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -1326,16 +1326,18 @@ Document::Document(const char* aContentT
       mUserGestureActivated(false),
       mStackRefCnt(0),
       mUpdateNestLevel(0),
       mViewportType(Unknown),
       mViewportOverflowType(ViewportOverflowType::NoOverflow),
       mSubDocuments(nullptr),
       mHeaderData(nullptr),
       mFlashClassification(FlashClassification::Unknown),
+      mScrollAnchorAdjustmentLength(0),
+      mScrollAnchorAdjustmentCount(0),
       mBoxObjectTable(nullptr),
       mCurrentOrientationAngle(0),
       mCurrentOrientationType(OrientationType::Portrait_primary),
       mServoRestyleRootDirtyBits(0),
       mThrowOnDynamicMarkupInsertionCounter(0),
       mIgnoreOpensDuringUnloadCounter(0),
       mDocLWTheme(Doc_Theme_Uninitialized),
       mSavedResolution(1.0f) {
@@ -6911,16 +6913,21 @@ void Document::UpdateViewportOverflowTyp
     if (aScrolledWidth * minScale.scale < aScrollportWidth) {
       mViewportOverflowType = ViewportOverflowType::ButNotMinScaleSize;
     } else {
       mViewportOverflowType = ViewportOverflowType::MinScaleSize;
     }
   }
 }
 
+void Document::UpdateForScrollAnchorAdjustment(nscoord aLength) {
+  mScrollAnchorAdjustmentLength += abs(aLength);
+  mScrollAnchorAdjustmentCount += 1;
+}
+
 EventListenerManager* Document::GetOrCreateListenerManager() {
   if (!mListenerManager) {
     mListenerManager =
         new EventListenerManager(static_cast<EventTarget*>(this));
     SetFlags(NODE_HAS_LISTENERMANAGER);
   }
 
   return mListenerManager;
@@ -11338,16 +11345,25 @@ void Document::ReportUseCounters(UseCoun
       CASE_OVERFLOW_TYPE(NoOverflow)
       CASE_OVERFLOW_TYPE(Desktop)
       CASE_OVERFLOW_TYPE(ButNotMinScaleSize)
       CASE_OVERFLOW_TYPE(MinScaleSize)
 #undef CASE_OVERFLOW_TYPE
     }
     Telemetry::AccumulateCategorical(label);
   }
+
+  if (IsTopLevelContentDocument()) {
+    CSSIntCoord adjustmentLength =
+        CSSPixel::FromAppUnits(mScrollAnchorAdjustmentLength).Rounded();
+    Telemetry::Accumulate(Telemetry::SCROLL_ANCHOR_ADJUSTMENT_LENGTH,
+                          adjustmentLength);
+    Telemetry::Accumulate(Telemetry::SCROLL_ANCHOR_ADJUSTMENT_COUNT,
+                          mScrollAnchorAdjustmentCount);
+  }
 }
 
 void Document::UpdateIntersectionObservations() {
   if (mIntersectionObservers.IsEmpty()) {
     return;
   }
 
   DOMHighResTimeStamp time = 0;
--- a/dom/base/Document.h
+++ b/dom/base/Document.h
@@ -1277,16 +1277,18 @@ class Document : public nsINode,
    * This should only be called when there is out-of-reach overflow
    * happens on the viewport, i.e. the viewport should be using
    * `overflow: hidden`. And it should only be called on a top level
    * content document.
    */
   void UpdateViewportOverflowType(nscoord aScrolledWidth,
                                   nscoord aScrollportWidth);
 
+  void UpdateForScrollAnchorAdjustment(nscoord aLength);
+
   /**
    * True iff this doc will ignore manual character encoding overrides.
    */
   virtual bool WillIgnoreCharsetOverride() { return true; }
 
   /**
    * Return whether the document was created by a srcdoc iframe.
    */
@@ -4336,16 +4338,19 @@ class Document : public nsINode,
 
   // Recorded time of change to 'loading' state.
   mozilla::TimeStamp mLoadingTimeStamp;
 
   nsWeakPtr mAutoFocusElement;
 
   nsCString mScrollToRef;
 
+  nscoord mScrollAnchorAdjustmentLength;
+  int32_t mScrollAnchorAdjustmentCount;
+
   // Weak reference to the scope object (aka the script global object)
   // that, unlike mScriptGlobalObject, is never unset once set. This
   // is a weak reference to avoid leaks due to circular references.
   nsWeakPtr mScopeObject;
 
   // Array of intersection observers
   nsTHashtable<nsPtrHashKey<mozilla::dom::DOMIntersectionObserver>>
       mIntersectionObservers;
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -1306,17 +1306,18 @@ void PresShell::Destroy() {
   mCurrentEventFrame = nullptr;
 
   int32_t i, count = mCurrentEventFrameStack.Length();
   for (i = 0; i < count; i++) {
     mCurrentEventFrameStack[i] = nullptr;
   }
 
   mFramesToDirty.Clear();
-  mDirtyScrollAnchorContainers.Clear();
+  mPendingScrollAnchorSelection.Clear();
+  mPendingScrollAnchorAdjustment.Clear();
 
   if (mViewManager) {
     // Clear the view manager's weak pointer back to |this| in case it
     // was leaked.
     mViewManager->SetPresShell(nullptr);
     mViewManager = nullptr;
   }
 
@@ -2146,17 +2147,18 @@ void PresShell::NotifyDestroyingFrame(ns
         mCurrentEventFrameStack[i] = nullptr;
       }
     }
 
     mFramesToDirty.RemoveEntry(aFrame);
 
     nsIScrollableFrame* scrollableFrame = do_QueryFrame(aFrame);
     if (scrollableFrame) {
-      mDirtyScrollAnchorContainers.RemoveEntry(scrollableFrame);
+      mPendingScrollAnchorSelection.RemoveEntry(scrollableFrame);
+      mPendingScrollAnchorAdjustment.RemoveEntry(scrollableFrame);
     }
   }
 }
 
 already_AddRefed<nsCaret> PresShell::GetCaret() const {
   RefPtr<nsCaret> caret = mCaret;
   return caret.forget();
 }
@@ -2569,27 +2571,42 @@ void PresShell::VerifyHasDirtyRootAncest
   }
 
   MOZ_ASSERT_UNREACHABLE(
       "Frame has dirty bits set but isn't scheduled to be "
       "reflowed?");
 }
 #endif
 
-void PresShell::PostDirtyScrollAnchorContainer(nsIScrollableFrame* aFrame) {
-  mDirtyScrollAnchorContainers.PutEntry(aFrame);
-}
-
-void PresShell::FlushDirtyScrollAnchorContainers() {
-  for (auto iter = mDirtyScrollAnchorContainers.Iter(); !iter.Done();
+void PresShell::PostPendingScrollAnchorSelection(
+    mozilla::layout::ScrollAnchorContainer* aContainer) {
+  mPendingScrollAnchorSelection.PutEntry(aContainer->ScrollableFrame());
+}
+
+void PresShell::FlushPendingScrollAnchorSelections() {
+  for (auto iter = mPendingScrollAnchorSelection.Iter(); !iter.Done();
        iter.Next()) {
     nsIScrollableFrame* scroll = iter.Get()->GetKey();
     scroll->GetAnchor()->SelectAnchor();
   }
-  mDirtyScrollAnchorContainers.Clear();
+  mPendingScrollAnchorSelection.Clear();
+}
+
+void PresShell::PostPendingScrollAnchorAdjustment(
+    ScrollAnchorContainer* aContainer) {
+  mPendingScrollAnchorAdjustment.PutEntry(aContainer->ScrollableFrame());
+}
+
+void PresShell::FlushPendingScrollAnchorAdjustments() {
+  for (auto iter = mPendingScrollAnchorAdjustment.Iter(); !iter.Done();
+       iter.Next()) {
+    nsIScrollableFrame* scroll = iter.Get()->GetKey();
+    scroll->GetAnchor()->ApplyAdjustments();
+  }
+  mPendingScrollAnchorAdjustment.Clear();
 }
 
 void PresShell::FrameNeedsReflow(nsIFrame* aFrame,
                                  IntrinsicDirty aIntrinsicDirty,
                                  nsFrameState aBitToAdd,
                                  ReflowRootHandling aRootHandling) {
   MOZ_ASSERT(aBitToAdd == NS_FRAME_IS_DIRTY ||
                  aBitToAdd == NS_FRAME_HAS_DIRTY_CHILDREN || !aBitToAdd,
@@ -4169,23 +4186,27 @@ void PresShell::DoFlushPendingNotificati
 
     if (flushType >= (SuppressInterruptibleReflows()
                           ? FlushType::Layout
                           : FlushType::InterruptibleLayout) &&
         !mIsDestroying) {
       didLayoutFlush = true;
       mFrameConstructor->RecalcQuotesAndCounters();
       viewManager->FlushDelayedResize(true);
-      if (ProcessReflowCommands(flushType < FlushType::Layout) &&
-          mContentToScrollTo) {
-        // We didn't get interrupted.  Go ahead and scroll to our content
-        DoScrollContentIntoView();
+      if (ProcessReflowCommands(flushType < FlushType::Layout)) {
+        // We didn't get interrupted. Go ahead and perform scroll anchor
+        // adjustments and scroll content into view
+        FlushPendingScrollAnchorAdjustments();
+
         if (mContentToScrollTo) {
-          mContentToScrollTo->DeleteProperty(nsGkAtoms::scrolling);
-          mContentToScrollTo = nullptr;
+          DoScrollContentIntoView();
+          if (mContentToScrollTo) {
+            mContentToScrollTo->DeleteProperty(nsGkAtoms::scrolling);
+            mContentToScrollTo = nullptr;
+          }
         }
       }
     }
 
     if (flushType >= FlushType::Layout) {
       if (!mIsDestroying) {
         viewManager->UpdateWidgetGeometry();
       }
@@ -8493,17 +8514,17 @@ bool PresShell::DoReflow(nsIFrame* targe
 #ifdef MOZ_GECKO_PROFILER
   DECLARE_DOCSHELL_AND_HISTORY_ID(docShell);
   AutoProfilerTracing tracingLayoutFlush("Paint", "Reflow",
                                          std::move(mReflowCause), docShellId,
                                          docShellHistoryId);
   mReflowCause = nullptr;
 #endif
 
-  FlushDirtyScrollAnchorContainers();
+  FlushPendingScrollAnchorSelections();
 
   if (mReflowContinueTimer) {
     mReflowContinueTimer->Cancel();
     mReflowContinueTimer = nullptr;
   }
 
   const bool isRoot = target == mFrameConstructor->GetRootFrame();
 
@@ -8616,17 +8637,17 @@ bool PresShell::DoReflow(nsIFrame* targe
       mPresContext, target, target->GetView(), boundsRelativeToTarget);
   nsContainerFrame::SyncWindowProperties(mPresContext, target,
                                          target->GetView(), rcx,
                                          nsContainerFrame::SET_ASYNC);
 
   target->DidReflow(mPresContext, nullptr);
   if (target->IsInScrollAnchorChain()) {
     ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(target);
-    container->ApplyAdjustments();
+    PostPendingScrollAnchorAdjustment(container);
   }
   if (isRoot && size.BSize(wm) == NS_UNCONSTRAINEDSIZE) {
     mPresContext->SetVisibleArea(boundsRelativeToTarget);
   }
 
 #ifdef DEBUG
   mCurrentReflowRoot = nullptr;
 #endif
@@ -10029,17 +10050,18 @@ void PresShell::AddSizeOfIncludingThis(n
   mFrameArena.AddSizeOfExcludingThis(aSizes);
   aSizes.mLayoutPresShellSize += mallocSizeOf(this);
   if (mCaret) {
     aSizes.mLayoutPresShellSize += mCaret->SizeOfIncludingThis(mallocSizeOf);
   }
   aSizes.mLayoutPresShellSize +=
       mApproximatelyVisibleFrames.ShallowSizeOfExcludingThis(mallocSizeOf) +
       mFramesToDirty.ShallowSizeOfExcludingThis(mallocSizeOf) +
-      mDirtyScrollAnchorContainers.ShallowSizeOfExcludingThis(mallocSizeOf);
+      mPendingScrollAnchorSelection.ShallowSizeOfExcludingThis(mallocSizeOf) +
+      mPendingScrollAnchorAdjustment.ShallowSizeOfExcludingThis(mallocSizeOf);
 
   StyleSet()->AddSizeOfIncludingThis(aSizes);
 
   aSizes.mLayoutTextRunsSize += SizeOfTextRuns(mallocSizeOf);
 
   aSizes.mLayoutPresContextSize +=
       mPresContext->SizeOfIncludingThis(mallocSizeOf);
 
--- a/layout/base/PresShell.h
+++ b/layout/base/PresShell.h
@@ -108,18 +108,22 @@ class PresShell final : public nsIPresSh
       nscoord aOldHeight = 0,
       ResizeReflowOptions aOptions = ResizeReflowOptions::eBSizeExact) override;
   nsresult ResizeReflowIgnoreOverride(
       nscoord aWidth, nscoord aHeight, nscoord aOldWidth, nscoord aOldHeight,
       ResizeReflowOptions aOptions = ResizeReflowOptions::eBSizeExact) override;
   nsIPageSequenceFrame* GetPageSequenceFrame() const override;
   nsCanvasFrame* GetCanvasFrame() const override;
 
-  void PostDirtyScrollAnchorContainer(nsIScrollableFrame* aFrame) override;
-  void FlushDirtyScrollAnchorContainers() override;
+  void PostPendingScrollAnchorSelection(
+      mozilla::layout::ScrollAnchorContainer* aContainer) override;
+  void FlushPendingScrollAnchorSelections() override;
+  void PostPendingScrollAnchorAdjustment(
+      mozilla::layout::ScrollAnchorContainer* aContainer) override;
+  void FlushPendingScrollAnchorAdjustments();
 
   void FrameNeedsReflow(
       nsIFrame* aFrame, IntrinsicDirty aIntrinsicDirty, nsFrameState aBitToAdd,
       ReflowRootHandling aRootHandling = eInferFromBitToAdd) override;
   void FrameNeedsToContinueReflow(nsIFrame* aFrame) override;
   void CancelAllPendingReflows() override;
   void DoFlushPendingNotifications(FlushType aType) override;
   void DoFlushPendingNotifications(ChangesToFlush aType) override;
@@ -742,17 +746,18 @@ class PresShell final : public nsIPresSh
   layers::ScrollableLayerGuid mMouseEventTargetGuid;
 
   // mStyleSet owns it but we maintain a ref, may be null
   RefPtr<StyleSheet> mPrefStyleSheet;
 
   // Set of frames that we should mark with NS_FRAME_HAS_DIRTY_CHILDREN after
   // we finish reflowing mCurrentReflowRoot.
   nsTHashtable<nsPtrHashKey<nsIFrame>> mFramesToDirty;
-  nsTHashtable<nsPtrHashKey<nsIScrollableFrame>> mDirtyScrollAnchorContainers;
+  nsTHashtable<nsPtrHashKey<nsIScrollableFrame>> mPendingScrollAnchorSelection;
+  nsTHashtable<nsPtrHashKey<nsIScrollableFrame>> mPendingScrollAnchorAdjustment;
 
   nsTArray<UniquePtr<DelayedEvent>> mDelayedEvents;
 
  private:
   nsRevocableEventPtr<nsSynthMouseMoveEvent> mSynthMouseMoveEvent;
   nsCOMPtr<nsIContent> mLastAnchorScrolledTo;
   RefPtr<nsCaret> mCaret;
   RefPtr<nsCaret> mOriginalCaret;
--- a/layout/base/RestyleManager.cpp
+++ b/layout/base/RestyleManager.cpp
@@ -771,17 +771,17 @@ static bool RecomputePosition(nsIFrame* 
         }
         cont->SetPosition(normalPosition +
                           nsPoint(newOffsets.left, newOffsets.top));
       }
     }
 
     if (aFrame->IsInScrollAnchorChain()) {
       ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(aFrame);
-      container->ApplyAdjustments();
+      aFrame->PresShell()->PostPendingScrollAnchorAdjustment(container);
     }
     return true;
   }
 
   // For the absolute positioning case, set up a fake HTML reflow state for
   // the frame, and then get the offsets and size from it. If the frame's size
   // doesn't need to change, we can simply update the frame position. Otherwise
   // we fall back to a reflow.
@@ -880,17 +880,17 @@ static bool RecomputePosition(nsIFrame* 
     nsPoint pos(parentBorder.left + reflowInput.ComputedPhysicalOffsets().left +
                     reflowInput.ComputedPhysicalMargin().left,
                 parentBorder.top + reflowInput.ComputedPhysicalOffsets().top +
                     reflowInput.ComputedPhysicalMargin().top);
     aFrame->SetPosition(pos);
 
     if (aFrame->IsInScrollAnchorChain()) {
       ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(aFrame);
-      container->ApplyAdjustments();
+      aFrame->PresShell()->PostPendingScrollAnchorAdjustment(container);
     }
     return true;
   }
 
   // Fall back to a reflow
   StyleChangeReflow(aFrame, nsChangeHint_NeedReflow |
                                 nsChangeHint_ReflowChangesSizeOrPosition);
   return false;
@@ -2967,17 +2967,17 @@ void RestyleManager::DoProcessPendingRes
     // fix up the callers and assert against this case, but we just detect and
     // handle it for now.
     return;
   }
 
   // Select scroll anchors for frames that have been scrolled. Do this
   // before restyling so that anchor nodes are correctly marked for
   // scroll anchor update suppressions.
-  presContext->PresShell()->FlushDirtyScrollAnchorContainers();
+  presContext->PresShell()->FlushPendingScrollAnchorSelections();
 
   // Create a AnimationsWithDestroyedFrame during restyling process to
   // stop animations and transitions on elements that have no frame at the end
   // of the restyling process.
   AnimationsWithDestroyedFrame animationsWithDestroyedFrame(this);
 
   ServoStyleSet* styleSet = StyleSet();
   Document* doc = presContext->Document();
--- a/layout/base/nsIPresShell.h
+++ b/layout/base/nsIPresShell.h
@@ -98,16 +98,20 @@ class Element;
 class Event;
 class Document;
 class HTMLSlotElement;
 class Touch;
 class Selection;
 class ShadowRoot;
 }  // namespace dom
 
+namespace layout {
+class ScrollAnchorContainer;
+}  // namespace layout
+
 namespace layers {
 class LayerManager;
 }  // namespace layers
 
 namespace gfx {
 class SourceSurface;
 }  // namespace gfx
 }  // namespace mozilla
@@ -451,18 +455,21 @@ class nsIPresShell : public nsStubDocume
   virtual nsIPageSequenceFrame* GetPageSequenceFrame() const = 0;
 
   /**
    * Returns the canvas frame associated with the frame hierarchy.
    * Returns nullptr if is XUL document.
    */
   virtual nsCanvasFrame* GetCanvasFrame() const = 0;
 
-  virtual void PostDirtyScrollAnchorContainer(nsIScrollableFrame* aFrame) = 0;
-  virtual void FlushDirtyScrollAnchorContainers() = 0;
+  virtual void PostPendingScrollAnchorSelection(
+      mozilla::layout::ScrollAnchorContainer* aContainer) = 0;
+  virtual void FlushPendingScrollAnchorSelections() = 0;
+  virtual void PostPendingScrollAnchorAdjustment(
+      mozilla::layout::ScrollAnchorContainer* aContainer) = 0;
 
   /**
    * Tell the pres shell that a frame needs to be marked dirty and needs
    * Reflow.  It's OK if this is an ancestor of the frame needing reflow as
    * long as the ancestor chain between them doesn't cross a reflow root.
    *
    * The bit to add should be NS_FRAME_IS_DIRTY, NS_FRAME_HAS_DIRTY_CHILDREN
    * or nsFrameState(0); passing 0 means that dirty bits won't be set on the
--- a/layout/generic/ScrollAnchorContainer.cpp
+++ b/layout/generic/ScrollAnchorContainer.cpp
@@ -222,17 +222,17 @@ void ScrollAnchorContainer::InvalidateAn
   ANCHOR_LOG("Invalidating scroll anchor %p for %p.\n", mAnchorNode, this);
 
   if (mAnchorNode) {
     SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, false);
   }
   mAnchorNode = nullptr;
   mAnchorNodeIsDirty = true;
   mLastAnchorPos = nsPoint();
-  Frame()->PresShell()->PostDirtyScrollAnchorContainer(ScrollableFrame());
+  Frame()->PresShell()->PostPendingScrollAnchorSelection(this);
 }
 
 void ScrollAnchorContainer::Destroy() {
   if (mAnchorNode) {
     SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, false);
   }
   mAnchorNode = nullptr;
   mAnchorNodeIsDirty = false;
@@ -285,16 +285,24 @@ void ScrollAnchorContainer::ApplyAdjustm
   MOZ_ASSERT(!mApplyingAnchorAdjustment);
   // We should use AutoRestore here, but that doesn't work with bitfields
   mApplyingAnchorAdjustment = true;
   mScrollFrame->ScrollBy(
       adjustmentDevicePixels, nsIScrollableFrame::DEVICE_PIXELS,
       nsIScrollableFrame::INSTANT, nullptr, nsGkAtoms::relative);
   mApplyingAnchorAdjustment = false;
 
+  nsPresContext* pc = Frame()->PresContext();
+  Document* doc = pc->Document();
+  if (writingMode.IsVertical()) {
+    doc->UpdateForScrollAnchorAdjustment(adjustment.x);
+  } else {
+    doc->UpdateForScrollAnchorAdjustment(adjustment.y);
+  }
+
   // The anchor position may not be in the same relative position after
   // adjustment. Update ourselves so we have consistent state.
   mLastAnchorPos =
       FindScrollAnchoringBoundingRect(Frame(), mAnchorNode).TopLeft();
 }
 
 ScrollAnchorContainer::ExamineResult
 ScrollAnchorContainer::ExamineAnchorCandidate(nsIFrame* aFrame) const {
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -1090,31 +1090,39 @@ void nsIFrame::MarkNeedsDisplayItemRebui
     if (oldBorder) {
       oldValue = oldBorder->GetComputedBorder();
       newValue = StyleBorder()->GetComputedBorder();
       if (oldValue != newValue && !HasProperty(UsedBorderProperty())) {
         AddProperty(UsedBorderProperty(), new nsMargin(oldValue));
       }
     }
 
+    const nsStyleDisplay* oldDisp = aOldComputedStyle->PeekStyleDisplay();
+    if (oldDisp &&
+        (oldDisp->mOverflowAnchor != StyleDisplay()->mOverflowAnchor)) {
+      ScrollAnchorContainer::FindFor(this)->InvalidateAnchor();
+      if (nsIScrollableFrame* scrollableFrame = do_QueryFrame(this)) {
+        scrollableFrame->GetAnchor()->InvalidateAnchor();
+      }
+    }
+
     if (mInScrollAnchorChain) {
       const nsStylePosition* oldPosition =
           aOldComputedStyle->PeekStylePosition();
       if (oldPosition &&
           (oldPosition->mOffset != StylePosition()->mOffset ||
            oldPosition->mWidth != StylePosition()->mWidth ||
            oldPosition->mMinWidth != StylePosition()->mMinWidth ||
            oldPosition->mMaxWidth != StylePosition()->mMaxWidth ||
            oldPosition->mHeight != StylePosition()->mHeight ||
            oldPosition->mMinHeight != StylePosition()->mMinHeight ||
            oldPosition->mMaxHeight != StylePosition()->mMaxHeight)) {
         needAnchorSuppression = true;
       }
 
-      const nsStyleDisplay* oldDisp = aOldComputedStyle->PeekStyleDisplay();
       if (oldDisp && (oldDisp->mPosition != StyleDisplay()->mPosition ||
                       oldDisp->TransformChanged(*StyleDisplay()))) {
         needAnchorSuppression = true;
       }
     }
 
     if (mInScrollAnchorChain && needAnchorSuppression) {
       ScrollAnchorContainer::FindFor(this)->SuppressAdjustments();
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -1131,17 +1131,17 @@ void nsHTMLScrollFrame::Reflow(nsPresCon
   aStatus.Reset();  // This type of frame can't be split.
   NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize);
   mHelper.PostOverflowEvent();
 }
 
 void nsHTMLScrollFrame::DidReflow(nsPresContext* aPresContext,
                                   const ReflowInput* aReflowInput) {
   nsContainerFrame::DidReflow(aPresContext, aReflowInput);
-  mHelper.mAnchor.ApplyAdjustments();
+  PresShell()->PostPendingScrollAnchorAdjustment(GetAnchor());
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 
 #ifdef DEBUG_FRAME_DUMP
 nsresult nsHTMLScrollFrame::GetFrameName(nsAString& aResult) const {
   return MakeFrameName(NS_LITERAL_STRING("HTMLScroll"), aResult);
 }
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/css/css-scroll-anchoring/anchoring-with-bounds-clamping-div.html.ini
@@ -0,0 +1,3 @@
+[anchoring-with-bounds-clamping-div.html]
+    [Anchoring combined with scroll bounds clamping in a <div>.]
+        expected: FAIL
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/heuristic-with-offset-update.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<html>
+<head>
+    <style type="text/css">
+        #scroller {
+            overflow: scroll;
+            height: 500px;
+            height: 500px;
+        }
+        #before {
+            height: 200px;
+        }
+        #anchor {
+            position: relative;
+            width: 200px;
+            height: 200px;
+            margin-bottom: 500px;
+            background-color: blue;
+            /*
+             * To trigger the Gecko bug that's being regression-tested here, we
+             * need 'top' to start out at a non-'auto' value, so that the
+             * dynamic change can trigger Gecko's "RecomputePosition" fast path
+             */
+            top: 0px;
+        }
+    </style>
+</head>
+<body>
+    <div id="scroller">
+        <div id="before">
+        </div>
+        <div id="anchor">
+        </div>
+    </div>
+
+    <script type="text/javascript">
+        test(() => {
+            let scroller = document.querySelector('#scroller');
+            let before = document.querySelector('#before');
+            let anchor = document.querySelector('#anchor');
+
+            // Scroll down to select #anchor as a scroll anchor
+            scroller.scrollTop = 200;
+
+            // Adjust the 'top' of #anchor, which should trigger a suppression
+            anchor.style.top = '10px';
+
+            // Expand #before and make sure we don't apply an adjustment
+            before.style.height = '300px';
+            assert_equals(scroller.scrollTop, 200);
+        }, 'Positioned ancestors with dynamic changes to offsets trigger scroll suppressions.');
+    </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic-scroller.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#scroller {
+  overflow: scroll;
+  width: 300px;
+  height: 300px;
+}
+#before { height: 50px; }
+#content { margin-top: 100px; margin-bottom: 600px; }
+.no { overflow-anchor: none; }
+
+</style>
+<div id="scroller">
+  <div id="before"></div>
+  <div id="content">content</div>
+</div>
+<script>
+
+// Tests that dynamic styling 'overflow-anchor' on a scrolling element has the
+// same effect as initial styling
+
+test(() => {
+  let scroller = document.querySelector("#scroller");
+  let before = document.querySelector("#before");
+
+  // Scroll down so that #content is the first element in the viewport
+  scroller.scrollTop = 100;
+
+  // Change the height of #before to trigger a scroll adjustment. This ensures
+  // that #content was selected as a scroll anchor
+  before.style.height = "100px";
+  assert_equals(scroller.scrollTop, 150);
+
+  // Now set 'overflow-anchor: none' on #scroller. This should invalidate the
+  // scroll anchor, and #scroller shouldn't be able to select an anchor anymore
+  scroller.className = 'no';
+
+  // Change the height of #before and make sure we don't adjust. This ensures
+  // that #content is not a scroll anchor
+  before.style.height = "150px";
+  assert_equals(scroller.scrollTop, 150);
+}, "Dynamically styling 'overflow-anchor: none' on the scroller element should prevent scroll anchoring");
+
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#scroller {
+  overflow: scroll;
+  width: 300px;
+  height: 300px;
+}
+#before { height: 50px; }
+#content { margin-top: 100px; margin-bottom: 600px; }
+.no { overflow-anchor: none; }
+
+</style>
+<div id="scroller">
+  <div id="before"></div>
+  <div id="content">content</div>
+</div>
+<script>
+
+// Tests that dynamic styling 'overflow-anchor' on an anchor node has the
+// same effect as initial styling
+
+test(() => {
+  let scroller = document.querySelector("#scroller");
+  let before = document.querySelector("#before");
+  let content = document.querySelector("#content");
+
+  // Scroll down so that #content is the first element in the viewport
+  scroller.scrollTop = 100;
+
+  // Change the height of #before to trigger a scroll adjustment. This ensures
+  // that #content was selected as a scroll anchor
+  before.style.height = "100px";
+  assert_equals(scroller.scrollTop, 150);
+
+  // Now set 'overflow-anchor: none' on #content. This should invalidate the
+  // scroll anchor, and #scroller should recalculate its anchor. There are no
+  // other valid anchors in the viewport, so there should be no anchor.
+  content.className = 'no';
+
+  // Change the height of #before and make sure we don't adjust. This ensures
+  // that #content was not selected as a scroll anchor
+  before.style.height = "150px";
+  assert_equals(scroller.scrollTop, 150);
+}, "Dynamically styling 'overflow-anchor: none' on the anchor node should prevent scroll anchoring");
+
+</script>
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -14527,10 +14527,36 @@
       "esawin@mozilla.com"
     ],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 5000,
     "n_buckets": 50,
     "bug_numbers": [1499418],
     "description": "GeckoView: Time taken to initialize all GeckoView modules in ms."
+  },
+  "SCROLL_ANCHOR_ADJUSTMENT_LENGTH": {
+    "record_in_processes": ["main", "content"],
+    "alert_emails": [
+      "rhunt@mozilla.com"
+    ],
+    "expires_in_version": "70",
+    "kind": "exponential",
+    "low": 1,
+    "high": 10000,
+    "n_buckets": 50,
+    "bug_numbers": [1518624],
+    "description": "The absolute length in CSS pixels of all scroll adjustments performed by scroll anchoring in a top-level document."
+  },
+  "SCROLL_ANCHOR_ADJUSTMENT_COUNT": {
+    "record_in_processes": ["main", "content"],
+    "alert_emails": [
+      "rhunt@mozilla.com"
+    ],
+    "expires_in_version": "70",
+    "kind": "exponential",
+    "low": 1,
+    "high": 500,
+    "n_buckets": 50,
+    "bug_numbers": [1518624],
+    "description": "The amount of scroll adjustments performed by scroll anchoring in a top-level document."
   }
 }
rename from toolkit/mozapps/update/tests/data/update.sjs
rename to toolkit/mozapps/update/tests/browser/app_update.sjs
--- a/toolkit/mozapps/update/tests/data/update.sjs
+++ b/toolkit/mozapps/update/tests/browser/app_update.sjs
@@ -34,18 +34,19 @@ scriptFile.append("testConstants.js");
 loadHelperScript(scriptFile);
 
 scriptFile = getTestDataFile("sharedUpdateXML.js");
 loadHelperScript(scriptFile);
 
 const SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + FILE_SIMPLE_MAR;
 const BAD_SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + "not_here.mar";
 
-const SLOW_MAR_DOWNLOAD_INTERVAL = 100;
-var gTimer;
+const SLOW_RESPONSE_INTERVAL = 10;
+var gSlowDownloadTimer;
+var gSlowCheckTimer;
 
 function handleRequest(aRequest, aResponse) {
   let params = { };
   if (aRequest.queryString) {
     params = parseQueryString(aRequest.queryString);
   }
 
   let statusCode = params.statusCode ? parseInt(params.statusCode) : 200;
@@ -57,64 +58,71 @@ function handleRequest(aRequest, aRespon
   // downloading before the ui has loaded. By specifying a serviceURL for the
   // update patch that points to this file and has a slowDownloadMar param the
   // mar will be downloaded asynchronously which will allow the ui to load
   // before the download completes.
   if (params.slowDownloadMar) {
     aResponse.processAsync();
     aResponse.setHeader("Content-Type", "binary/octet-stream");
     aResponse.setHeader("Content-Length", SIZE_SIMPLE_MAR);
-    var continueFile = getTestDataFile("continue");
+    var continueFile = getTestDataFile(CONTINUE_DOWNLOAD);
     var contents = readFileBytes(getTestDataFile(FILE_SIMPLE_MAR));
-    gTimer = Cc["@mozilla.org/timer;1"].
-             createInstance(Ci.nsITimer);
-    gTimer.initWithCallback(function(aTimer) {
+    gSlowDownloadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    gSlowDownloadTimer.initWithCallback(function(aTimer) {
       if (continueFile.exists()) {
-        gTimer.cancel();
-        aResponse.write(contents);
-        aResponse.finish();
+        try {
+          // If the continue file is in use try again the next time the timer
+          // fires.
+          continueFile.remove(false);
+          gSlowDownloadTimer.cancel();
+          aResponse.write(contents);
+          aResponse.finish();
+        } catch (e) {
+        }
       }
-    }, SLOW_MAR_DOWNLOAD_INTERVAL, Ci.nsITimer.TYPE_REPEATING_SLACK);
+    }, SLOW_RESPONSE_INTERVAL, Ci.nsITimer.TYPE_REPEATING_SLACK);
     return;
   }
 
   if (params.uiURL) {
-    let remoteType = "";
-    if (!params.remoteNoTypeAttr && params.uiURL == "BILLBOARD") {
-      remoteType = " " + params.uiURL.toLowerCase() + "=\"1\"";
-    }
     aResponse.write("<html><head><meta http-equiv=\"content-type\" content=" +
-                    "\"text/html; charset=utf-8\"></head><body" +
-                    remoteType + ">" + params.uiURL +
-                    "<br><br>this is a test mar that will not affect your " +
-                    "build.</body></html>");
+                    "\"text/html; charset=utf-8\"></head><body>" +
+                    params.uiURL + "<br><br>this is a test mar that will not " +
+                    "affect your build.</body></html>");
     return;
   }
 
   if (params.xmlMalformed) {
-    aResponse.write("xml error");
+    respond(aResponse, params, "xml error");
     return;
   }
 
   if (params.noUpdates) {
-    aResponse.write(getRemoteUpdatesXMLString(""));
+    respond(aResponse, params, getRemoteUpdatesXMLString(""));
     return;
   }
 
   if (params.unsupported) {
-    aResponse.write(getRemoteUpdatesXMLString("  <update type=\"major\" " +
-                                              "unsupported=\"true\" " +
-                                              "detailsURL=\"" + URL_HOST +
-                                              "\"></update>\n"));
+    let detailsURL = params.detailsURL ? params.detailsURL : URL_HOST;
+    let unsupportedXML = getRemoteUpdatesXMLString("  <update type=\"major\" " +
+                                                   "unsupported=\"true\" " +
+                                                   "detailsURL=\"" + detailsURL +
+                                                   "\"></update>\n");
+    respond(aResponse, params, unsupportedXML);
     return;
   }
 
   let size;
   let patches = "";
-  let url = params.badURL ? BAD_SERVICE_URL : SERVICE_URL;
+  let url = "";
+  if (params.useSlowDownloadMar) {
+    url = URL_HTTP_UPDATE_SJS + "?slowDownloadMar=1"
+  } else {
+    url = params.badURL ? BAD_SERVICE_URL : SERVICE_URL
+  }
   if (!params.partialPatchOnly) {
     size = SIZE_SIMPLE_MAR + (params.invalidCompleteSize ? "1" : "");
     let patchProps = {type: "complete",
                       url: url,
                       size: size};
     patches += getRemotePatchString(patchProps);
   }
 
@@ -147,17 +155,41 @@ function handleRequest(aRequest, aRespon
     updateProps.buildID = params.buildID;
   }
 
   if (params.promptWaitTime) {
     updateProps.promptWaitTime = params.promptWaitTime;
   }
 
   let updates = getRemoteUpdateString(updateProps, patches);
-  aResponse.write(getRemoteUpdatesXMLString(updates));
+  let xml = getRemoteUpdatesXMLString(updates);
+  respond(aResponse, params, xml);
+}
+
+function respond(aResponse, aParams, aResponseString) {
+  if (aParams.slowUpdateCheck) {
+    aResponse.processAsync();
+    var continueFile = getTestDataFile(CONTINUE_CHECK);
+    gSlowCheckTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    gSlowCheckTimer.initWithCallback(function(aTimer) {
+      if (continueFile.exists()) {
+        try {
+          // If the continue file is in use try again the next time the timer
+          // fires.
+          continueFile.remove(false);
+          gSlowCheckTimer.cancel();
+          aResponse.write(aResponseString);
+          aResponse.finish();
+        } catch (e) {
+        }
+      }
+    }, SLOW_RESPONSE_INTERVAL, Ci.nsITimer.TYPE_REPEATING_SLACK);
+  } else {
+    aResponse.write(aResponseString);
+  }
 }
 
 /**
  * Helper function to create a JS object representing the url parameters from
  * the request's queryString.
  *
  * @param  aQueryString
  *         The request's query string.
--- a/toolkit/mozapps/update/tests/browser/browser.ini
+++ b/toolkit/mozapps/update/tests/browser/browser.ini
@@ -1,27 +1,51 @@
 [DEFAULT]
 tags = appupdate
 support-files =
   head.js
   downloadPage.html
   testConstants.js
+  app_update.sjs
 
+[browser_about_bc_downloaded.js]
+[browser_about_bc_downloaded_staged.js]
+skip-if = (os == "linux" && verify) || asan
+reason = Bug 1520672 and Bug 1168003
+[browser_about_fc_check_cantApply.js]
+skip-if = os != 'win'
+reason = test must be able to prevent file deletion.
+[browser_about_fc_check_malformedXML.js]
+[browser_about_fc_check_noUpdate.js]
+[browser_about_fc_check_unsupported.js]
+[browser_about_fc_downloadAuto.js]
+[browser_about_fc_downloadAuto_staging.js]
+skip-if = (os == "linux" && verify) || asan
+reason = Bug 1520672 and Bug 1168003
+[browser_about_fc_downloadOptIn.js]
+[browser_about_fc_downloadOptIn_staging.js]
+skip-if = (os == "linux" && verify) || asan
+reason = Bug 1520672 and Bug 1168003
+[browser_about_fc_patch_completeBadSize.js]
+[browser_about_fc_patch_partialBadSize.js]
+[browser_about_fc_patch_partialBadSize_complete.js]
+[browser_about_fc_patch_partialBadSize_completeBadSize.js]
 [browser_TelemetryUpdatePing.js]
 [browser_updateAutoPrefUI.js]
 skip-if = os != 'win'
 reason = Tests that update config is properly written to file, which is a Windows-only feature
 [browser_updatesBackgroundWindow.js]
 [browser_updatesBackgroundWindowFailures.js]
 [browser_updatesBasicPrompt.js]
-skip-if = asan
-reason = Bug 1168003
+skip-if = (os == "linux" && verify) || asan
+reason = Bug 1520672 and Bug 1168003
 [browser_updatesBasicPromptNoStaging.js]
 [browser_updatesCantApply.js]
 skip-if = os != 'win'
+reason = test must be able to prevent file deletion.
 [browser_updatesCompleteAndPartialPatchesWithBadCompleteSize.js]
 [browser_updatesCompleteAndPartialPatchesWithBadPartialSize.js]
 [browser_updatesCompleteAndPartialPatchesWithBadSizes.js]
 [browser_updatesCompletePatchApplyFailure.js]
 [browser_updatesCompletePatchWithBadCompleteSize.js]
 [browser_updatesDownloadFailures.js]
 [browser_updatesMalformedXml.js]
 [browser_updatesPartialPatchApplyFailure.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_bc_downloaded.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog background check for updates
+// with the update downloaded.
+add_task(async function aboutDialog_backgroundCheck_downloaded() {
+  let updateParams = "";
+  await runAboutDialogUpdateTest(updateParams, true, [
+    {
+      panelId: "apply",
+      checkActiveUpdate: {state: STATE_PENDING},
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_bc_downloaded_staged.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog background check for updates
+// with the update downloaded and staged.
+add_task(async function aboutDialog_backgroundCheck_downloaded_staged() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      [PREF_APP_UPDATE_STAGING_ENABLED, true],
+    ],
+  });
+
+  // Since the partial should be successful specify an invalid size for the
+  // complete update.
+  let updateParams = "&invalidCompleteSize=1";
+  await runAboutDialogUpdateTest(updateParams, true, [
+    {
+      panelId: "apply",
+      checkActiveUpdate: {state: STATE_APPLIED},
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_check_cantApply.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// without the ability to apply updates.
+add_task(async function aboutDialog_foregroundCheck_cantApply() {
+  lockWriteTestFile();
+
+  let updateParams = "";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "manualUpdate",
+      checkActiveUpdate: null,
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_check_malformedXML.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// with a malformed update XML file.
+add_task(async function aboutDialog_foregroundCheck_malformedXML() {
+  let updateParams = "&xmlMalformed=1";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "noUpdatesFound",
+      checkActiveUpdate: null,
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_check_noUpdate.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// with no update available.
+add_task(async function aboutDialog_foregroundCheck_noUpdate() {
+  let updateParams = "&noUpdates=1";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "noUpdatesFound",
+      checkActiveUpdate: null,
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_check_unsupported.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// with an unsupported update.
+add_task(async function aboutDialog_foregroundCheck_unsupported() {
+  let updateParams = "&unsupported=1";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "unsupportedSystem",
+      checkActiveUpdate: null,
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_downloadAuto.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// with an automatic download.
+add_task(async function aboutDialog_foregroundCheck_downloadAuto() {
+  // Since the partial should be successful specify an invalid size for the
+  // complete update.
+  let updateParams = "&invalidCompleteSize=1";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "downloading",
+      checkActiveUpdate: {state: STATE_DOWNLOADING},
+      continueFile: CONTINUE_DOWNLOAD,
+    },
+    {
+      panelId: "apply",
+      checkActiveUpdate: {state: STATE_PENDING},
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_downloadAuto_staging.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// with an automatic download and update staging.
+add_task(async function aboutDialog_foregroundCheck_downloadAuto_staging() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      [PREF_APP_UPDATE_STAGING_ENABLED, true],
+    ],
+  });
+
+  // Since the partial should be successful specify an invalid size for the
+  // complete update.
+  let updateParams = "&invalidCompleteSize=1";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "downloading",
+      checkActiveUpdate: {state: STATE_DOWNLOADING},
+      continueFile: CONTINUE_DOWNLOAD,
+    },
+    {
+      panelId: "applying",
+      checkActiveUpdate: {state: STATE_PENDING},
+      continueFile: CONTINUE_STAGING,
+    },
+    {
+      panelId: "apply",
+      checkActiveUpdate: {state: STATE_APPLIED},
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_downloadOptIn.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// with a manual download.
+add_task(async function aboutDialog_foregroundCheck_downloadOptIn() {
+  await UpdateUtils.setAppUpdateAutoEnabled(false);
+
+  // Since the partial should be successful specify an invalid size for the
+  // complete update.
+  let updateParams = "&invalidCompleteSize=1";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "downloadAndInstall",
+      checkActiveUpdate: null,
+      continueFile: null,
+    },
+    {
+      panelId: "downloading",
+      checkActiveUpdate: {state: STATE_DOWNLOADING},
+      continueFile: CONTINUE_DOWNLOAD,
+    },
+    {
+      panelId: "apply",
+      checkActiveUpdate: {state: STATE_PENDING},
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_downloadOptIn_staging.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// with a manual download and update staging.
+add_task(async function aboutDialog_foregroundCheck_downloadOptIn_staging() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      [PREF_APP_UPDATE_STAGING_ENABLED, true],
+    ],
+  });
+  await UpdateUtils.setAppUpdateAutoEnabled(false);
+
+  // Since the partial should be successful specify an invalid size for the
+  // complete update.
+  let updateParams = "&invalidCompleteSize=1";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "downloadAndInstall",
+      checkActiveUpdate: null,
+      continueFile: null,
+    },
+    {
+      panelId: "downloading",
+      checkActiveUpdate: {state: STATE_DOWNLOADING},
+      continueFile: CONTINUE_DOWNLOAD,
+    },
+    {
+      panelId: "applying",
+      checkActiveUpdate: {state: STATE_PENDING},
+      continueFile: CONTINUE_STAGING,
+    },
+    {
+      panelId: "apply",
+      checkActiveUpdate: {state: STATE_APPLIED},
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_patch_completeBadSize.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// with a complete bad size patch.
+add_task(async function aboutDialog_foregroundCheck_completeBadSize() {
+  let updateParams = "&completePatchOnly=1&invalidCompleteSize=1";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "downloading",
+      checkActiveUpdate: {state: STATE_DOWNLOADING},
+      continueFile: CONTINUE_DOWNLOAD,
+    },
+    {
+      panelId: "downloadFailed",
+      checkActiveUpdate: null,
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_patch_partialBadSize.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// with a partial bad size patch.
+add_task(async function aboutDialog_foregroundCheck_partialBadSize() {
+  let updateParams = "&partialPatchOnly=1&invalidPartialSize=1";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "downloading",
+      checkActiveUpdate: {state: STATE_DOWNLOADING},
+      continueFile: CONTINUE_DOWNLOAD,
+    },
+    {
+      panelId: "downloadFailed",
+      checkActiveUpdate: null,
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_patch_partialBadSize_complete.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// with a partial bad size patch and a complete patch.
+add_task(async function aboutDialog_foregroundCheck_partialBadSize_complete() {
+  let updateParams = "&invalidPartialSize=1";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "downloading",
+      checkActiveUpdate: {state: STATE_DOWNLOADING},
+      continueFile: CONTINUE_DOWNLOAD,
+    },
+    {
+      panelId: "downloading",
+      checkActiveUpdate: {state: STATE_DOWNLOADING},
+      continueFile: CONTINUE_DOWNLOAD,
+    },
+    {
+      panelId: "apply",
+      checkActiveUpdate: {state: STATE_PENDING},
+      continueFile: null,
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/browser/browser_about_fc_patch_partialBadSize_completeBadSize.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for About Dialog foreground check for updates
+// with a partial bad size patch and a complete bad size patch.
+add_task(async function aboutDialog_foregroundCheck_partialBadSize_completeBadSize() {
+  let updateParams = "&invalidPartialSize=1&invalidCompleteSize=1";
+  await runAboutDialogUpdateTest(updateParams, false, [
+    {
+      panelId: "checkingForUpdates",
+      checkActiveUpdate: null,
+      continueFile: CONTINUE_CHECK,
+    },
+    {
+      panelId: "downloading",
+      checkActiveUpdate: {state: STATE_DOWNLOADING},
+      continueFile: CONTINUE_DOWNLOAD,
+    },
+    {
+      panelId: "downloading",
+      checkActiveUpdate: {state: STATE_DOWNLOADING},
+      continueFile: CONTINUE_DOWNLOAD,
+    },
+    {
+      panelId: "downloadFailed",
+      checkActiveUpdate: null,
+      continueFile: null,
+    },
+  ]);
+});
--- a/toolkit/mozapps/update/tests/browser/browser_updatesBasicPrompt.js
+++ b/toolkit/mozapps/update/tests/browser/browser_updatesBasicPrompt.js
@@ -1,16 +1,15 @@
 add_task(async function testBasicPrompt() {
   SpecialPowers.pushPrefEnv({set: [
     [PREF_APP_UPDATE_STAGING_ENABLED, true],
   ]});
   await UpdateUtils.setAppUpdateAutoEnabled(false);
 
   let updateParams = "promptWaitTime=0";
-  gUseTestUpdater = true;
 
   await runUpdateTest(updateParams, 1, [
     {
       notificationId: "update-available",
       button: "button",
       beforeClick() {
         checkWhatsNewLink(window, "update-available-whats-new");
       },
--- a/toolkit/mozapps/update/tests/browser/browser_updatesCantApply.js
+++ b/toolkit/mozapps/update/tests/browser/browser_updatesCantApply.js
@@ -1,36 +1,19 @@
 add_task(async function testBasicPrompt() {
   SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_SERVICE_ENABLED, false]]});
+  lockWriteTestFile();
 
   let updateParams = "promptWaitTime=0";
 
-  let file = getWriteTestFile();
-  file.create(file.NORMAL_FILE_TYPE, 0o444);
-  file.fileAttributesWin |= file.WFA_READONLY;
-  file.fileAttributesWin &= ~file.WFA_READWRITE;
-
   await runUpdateTest(updateParams, 1, [
     {
       notificationId: "update-manual",
       button: "button",
       async cleanup() {
         await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
         is(gBrowser.selectedBrowser.currentURI.spec,
            URL_MANUAL_UPDATE, "Landed on manual update page.");
         gBrowser.removeTab(gBrowser.selectedTab);
-        getWriteTestFile();
       },
     },
   ]);
 });
-
-function getWriteTestFile() {
-  let file = getUpdatesRootDir();
-  file.append(FILE_UPDATE_TEST);
-  file.QueryInterface(Ci.nsILocalFileWin);
-  if (file.exists()) {
-    file.fileAttributesWin |= file.WFA_READWRITE;
-    file.fileAttributesWin &= ~file.WFA_READONLY;
-    file.remove(true);
-  }
-  return file;
-}
--- a/toolkit/mozapps/update/tests/browser/head.js
+++ b/toolkit/mozapps/update/tests/browser/head.js
@@ -11,65 +11,134 @@ const IS_WIN = ("@mozilla.org/windows-re
 
 const BIN_SUFFIX = (IS_WIN ? ".exe" : "");
 const FILE_UPDATER_BIN = "updater" + (IS_MACOSX ? ".app" : BIN_SUFFIX);
 const FILE_UPDATER_BIN_BAK = FILE_UPDATER_BIN + ".bak";
 
 const PREF_APP_UPDATE_INTERVAL = "app.update.interval";
 const PREF_APP_UPDATE_LASTUPDATETIME = "app.update.lastUpdateTime.background-update-timer";
 
-let gRembemberedPrefs = [];
-
 const DATA_URI_SPEC =  "chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/";
 
 var DEBUG_AUS_TEST = true;
-var gUseTestUpdater = false;
 
 const LOG_FUNCTION = info;
 
 const MAX_UPDATE_COPY_ATTEMPTS = 10;
 
 /* import-globals-from testConstants.js */
 Services.scriptloader.loadSubScript(DATA_URI_SPEC + "testConstants.js", this);
 /* import-globals-from ../data/shared.js */
 Services.scriptloader.loadSubScript(DATA_URI_SPEC + "shared.js", this);
 
 var gURLData = URL_HOST + "/" + REL_PATH_DATA;
 const URL_MANUAL_UPDATE = gURLData + "downloadPage.html";
 
 const gEnv = Cc["@mozilla.org/process/environment;1"].
              getService(Ci.nsIEnvironment);
 
-const NOTIFICATIONS = [
-  "update-available",
-  "update-manual",
-  "update-restart",
-];
-
 let gOriginalUpdateAutoValue = null;
 
 /**
- * Delay for a very short period. Useful for moving the code after this
- * to the back of the event loop.
+ * Creates the continue file used to signal that update staging or the mock http
+ * server should continue. The delay this creates allows the tests to verify the
+ * user interfaces before they auto advance to phases of an update. The continue
+ * file for staging will be deleted by the test updater and the continue file
+ * for update check and update download requests will be deleted by the test
+ * http server handler implemented in app_update.sjs. The test returns a promise
+ * so the test can wait on the deletion of the continue file when necessary.
  *
- * @return A promise which will resolve after a very short period.
+ * @param  leafName
+ *         The leafName of the file to create. This should be one of the
+ *         folowing constants that are defined in testConstants.js:
+ *         CONTINUE_CHECK
+ *         CONTINUE_DOWNLOAD
+ *         CONTINUE_STAGING
+ * @return Promise
+ *         Resolves when the file is deleted.
+ *         Rejects if timeout is exceeded or condition ever throws.
+ * @throws If the file already exists.
  */
-function delay() {
-  return new Promise(resolve => executeSoon(resolve));
+async function continueFileHandler(leafName) {
+  // The default number of retries of 50 in TestUtils.waitForCondition is
+  // sufficient for test http server requests. The total time to wait with the
+  // default interval of 100 is approximately 5 seconds.
+  let retries = undefined;
+  let continueFile;
+  if (leafName == CONTINUE_STAGING) {
+    debugDump("creating " + leafName + " file for slow update staging");
+    // Use 100 retries for staging requests to lessen the likelihood of tests
+    // intermittently failing on debug builds due to launching the updater. The
+    // total time to wait with the default interval of 100 is approximately 10
+    // seconds. The test updater uses the same values.
+    retries = 100;
+    continueFile = getUpdatesPatchDir();
+    continueFile.append(leafName);
+  } else {
+    debugDump("creating " + leafName + " file for slow http server requests");
+    continueFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+    let continuePath = REL_PATH_DATA + leafName;
+    let continuePathParts = continuePath.split("/");
+    for (let i = 0; i < continuePathParts.length; ++i) {
+      continueFile.append(continuePathParts[i]);
+    }
+  }
+  if (continueFile.exists()) {
+    throw new Error("The continue file should not exist, path: " +
+                    continueFile.path);
+  }
+  continueFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+  return BrowserTestUtils.waitForCondition(() =>
+    (!continueFile.exists()),
+    "Waiting for file to be deleted, path: " + continueFile.path,
+    undefined, retries);
+
+}
+
+/**
+ * Creates and locks the app update write test file so it is possible to test
+ * when the user doesn't have write access to update. Since this is only
+ * possible on Windows the function throws when it is called on other platforms.
+ * This uses registerCleanupFunction to remove the lock and the file when the
+ * test completes.
+ *
+ * @throws If the function is called on a platform other than Windows.
+ */
+function lockWriteTestFile() {
+  if (AppConstants.platform != "win") {
+    throw new Error("Windows only test function called");
+  }
+  let file = getUpdatesRootDir();
+  file.append(FILE_UPDATE_TEST);
+  file.QueryInterface(Ci.nsILocalFileWin);
+  // Remove the file if it exists just in case.
+  if (file.exists()) {
+    file.fileAttributesWin |= file.WFA_READWRITE;
+    file.fileAttributesWin &= ~file.WFA_READONLY;
+    file.remove(false);
+  }
+  file.create(file.NORMAL_FILE_TYPE, 0o444);
+  file.fileAttributesWin |= file.WFA_READONLY;
+  file.fileAttributesWin &= ~file.WFA_READWRITE;
+  registerCleanupFunction(() => {
+    file.fileAttributesWin |= file.WFA_READWRITE;
+    file.fileAttributesWin &= ~file.WFA_READONLY;
+    file.remove(false);
+  });
 }
 
 /**
  * Gets the update version info for the update url parameters to send to
- * update.sjs.
+ * app_update.sjs.
  *
  * @param  aAppVersion (optional)
  *         The application version for the update snippet. If not specified the
  *         current application version will be used.
  * @return The url parameters for the application and platform version to send
- *         to update.sjs.
+ *         to app_update.sjs.
  */
 function getVersionParams(aAppVersion) {
   let appInfo = Services.appinfo;
   return "&appVersion=" + (aAppVersion ? aAppVersion : appInfo.version);
 }
 
 /**
  * Clean up updates list and the updates directory.
@@ -119,17 +188,17 @@ add_task(async function setDefaults() {
 });
 
 /**
  * Runs a typical update test. Will set various common prefs for using the
  * updater doorhanger, runs the provided list of steps, and makes sure
  * everything is cleaned up afterwards.
  *
  * @param  updateParams
- *         URL-encoded params which will be sent to update.sjs.
+ *         Params which will be sent to app_update.sjs.
  * @param  checkAttempts
  *         How many times to check for updates. Useful for testing the UI
  *         for check failures.
  * @param  steps
  *         A list of test steps to perform, specifying expected doorhangers
  *         and additional validation/cleanup callbacks.
  * @return A promise which will resolve once all of the steps have been run
  *         and cleanup has been performed.
@@ -313,31 +382,30 @@ function checkWhatsNewLink(win, id, url)
   let whatsNewLink = win.document.getElementById(id);
   is(whatsNewLink.href,
      url || URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS",
      "What's new link points to the test_details URL");
   is(whatsNewLink.hidden, false, "What's new link is not hidden.");
 }
 
 /**
- * For tests that use the test updater restores the backed up real updater if
- * it exists and tries again on failure since Windows debug builds at times
- * leave the file in use. After success moveRealUpdater is called to continue
- * the setup of the test updater. For tests that don't use the test updater
- * runTest will be called.
+ * For staging tests the test updater must be used and this restores the backed
+ * up real updater if it exists and tries again on failure since Windows debug
+ * builds at times leave the file in use. After success moveRealUpdater is
+ * called to continue the setup of the test updater.
  */
 function setupTestUpdater() {
   return (async function() {
-    if (gUseTestUpdater) {
+    if (Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED)) {
       try {
         restoreUpdaterBackup();
       } catch (e) {
         logTestInfo("Attempt to restore the backed up updater failed... " +
                     "will try again, Exception: " + e);
-        await delay();
+        await TestUtils.waitForTick();
         await setupTestUpdater();
         return;
       }
       await moveRealUpdater();
     }
   })();
 }
 
@@ -352,29 +420,28 @@ function moveRealUpdater() {
       // Move away the real updater
       let baseAppDir = getAppBaseDir();
       let updater = baseAppDir.clone();
       updater.append(FILE_UPDATER_BIN);
       updater.moveTo(baseAppDir, FILE_UPDATER_BIN_BAK);
     } catch (e) {
       logTestInfo("Attempt to move the real updater out of the way failed... " +
                   "will try again, Exception: " + e);
-      await delay();
+      await TestUtils.waitForTick();
       await moveRealUpdater();
       return;
     }
 
     await copyTestUpdater();
   })();
 }
 
 /**
- * Copies the test updater so it can be used by tests and tries again on failure
- * since Windows debug builds at times leave the file in use. After success it
- * will call runTest to continue the test.
+ * Copies the test updater and tries again on failure since Windows debug builds
+ * at times leave the file in use.
  */
 function copyTestUpdater(attempt = 0) {
   return (async function() {
     try {
       // Copy the test updater
       let baseAppDir = getAppBaseDir();
       let testUpdaterDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
       let relPath = REL_PATH_DATA;
@@ -386,58 +453,215 @@ function copyTestUpdater(attempt = 0) {
       let testUpdater = testUpdaterDir.clone();
       testUpdater.append(FILE_UPDATER_BIN);
 
       testUpdater.copyToFollowingLinks(baseAppDir, FILE_UPDATER_BIN);
     } catch (e) {
       if (attempt < MAX_UPDATE_COPY_ATTEMPTS) {
         logTestInfo("Attempt to copy the test updater failed... " +
                     "will try again, Exception: " + e);
-        await delay();
+        await TestUtils.waitForTick();
         await copyTestUpdater(attempt + 1);
       }
     }
   })();
 }
 
 /**
  * Restores the updater that was backed up. This is called in setupTestUpdater
  * before the backup of the real updater is done in case the previous test
- * failed to restore the updater, in finishTestDefaultWaitForWindowClosed when
- * the test has finished, and in test_9999_cleanup.xul after all tests have
- * finished.
+ * failed to restore the updater when the test has finished.
  */
 function restoreUpdaterBackup() {
   let baseAppDir = getAppBaseDir();
   let updater = baseAppDir.clone();
   let updaterBackup = baseAppDir.clone();
   updater.append(FILE_UPDATER_BIN);
   updaterBackup.append(FILE_UPDATER_BIN_BAK);
   if (updaterBackup.exists()) {
     if (updater.exists()) {
       updater.remove(true);
     }
     updaterBackup.moveTo(baseAppDir, FILE_UPDATER_BIN);
   }
 }
 
 /**
- * When a test finishes this will repeatedly attempt to restore the real updater
- * for tests that use the test updater and then call
- * finishTestDefaultWaitForWindowClosed after the restore is successful.
+ * When a staging test finishes this will repeatedly attempt to restore the real
+ * updater.
  */
 function finishTestRestoreUpdaterBackup() {
   return (async function() {
-    if (gUseTestUpdater) {
+    if (Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED)) {
       try {
         // Windows debug builds keep the updater file in use for a short period of
         // time after the updater process exits.
         restoreUpdaterBackup();
       } catch (e) {
         logTestInfo("Attempt to restore the backed up updater failed... " +
                     "will try again, Exception: " + e);
 
-        await delay();
+        await TestUtils.waitForTick();
         await finishTestRestoreUpdaterBackup();
       }
     }
   })();
 }
+
+/**
+ * Waits for the About Dialog to load.
+ *
+ * @return A promise that returns the domWindow for the About Dialog and
+ *         resolves when the About Dialog loads.
+ */
+function waitForAboutDialog() {
+  return new Promise(resolve => {
+    var listener = {
+      onOpenWindow: aXULWindow => {
+        debugDump("About dialog shown...");
+        Services.wm.removeListener(listener);
+
+         async function aboutDialogOnLoad() {
+          domwindow.removeEventListener("load", aboutDialogOnLoad, true);
+          let chromeURI = "chrome://browser/content/aboutDialog.xul";
+          is(domwindow.document.location.href, chromeURI, "About dialog appeared");
+          resolve(domwindow);
+        }
+
+        var domwindow = aXULWindow.docShell.domWindow;
+        domwindow.addEventListener("load", aboutDialogOnLoad, true);
+      },
+      onCloseWindow: aXULWindow => {},
+    };
+
+    Services.wm.addListener(listener);
+    openAboutDialog();
+  });
+}
+
+/**
+ * Runs an About Dialog update test. This will set various common prefs for
+ * updating and runs the provided list of steps.
+ *
+ * @param  updateParams
+ *         Params which will be sent to app_update.sjs.
+ * @param  backgroundUpdate
+ *         If true a background check will be performed before opening the About
+ *         Dialog.
+ * @param  steps
+ *         An array of test steps to perform. A step will either be an object
+ *         containing expected conditions and actions or a function to call.
+ * @return A promise which will resolve once all of the steps have been run.
+ */
+function runAboutDialogUpdateTest(updateParams, backgroundUpdate, steps) {
+  let aboutDialog;
+  function processAboutDialogStep(step) {
+    if (typeof(step) == "function") {
+      return step();
+    }
+
+    // Helper function to get the selected panel.
+    function getSelectedPanel() {
+      return aboutDialog.document.getElementById("updateDeck").selectedPanel;
+    }
+
+    // Helper function to get the selected panel's button.
+    function getSelectedPanelButton() {
+      return getSelectedPanel().querySelector("button");
+    }
+
+    // Helper function to get the selected panel's label with a class of
+    // text-link.
+    function getSelectedLabelLink() {
+      return getSelectedPanel().querySelector("label.text-link");
+    }
+
+    const {panelId, checkActiveUpdate, continueFile} = step;
+    return (async function() {
+      await BrowserTestUtils.waitForCondition(() =>
+        (getSelectedPanel() && getSelectedPanel().id == panelId),
+        "Waiting for expected panel ID - expected \"" + panelId + "\"");
+
+      // Skip when checkActiveUpdate evaluates to false.
+      if (checkActiveUpdate) {
+        ok(!!gUpdateManager.activeUpdate, "There should be an active update");
+        is(gUpdateManager.activeUpdate.state, checkActiveUpdate.state,
+           "The active update state should equal " + checkActiveUpdate.state);
+      } else {
+        ok(!gUpdateManager.activeUpdate,
+           "There should not be an active update");
+      }
+
+      if (continueFile) {
+        await continueFileHandler(continueFile);
+      }
+
+      let linkPanels = ["downloadFailed", "manualUpdate", "unsupportedSystem"];
+      if (linkPanels.includes(panelId)) {
+        // The unsupportedSystem panel uses the update's detailsURL and the
+        // downloadFailed and manualUpdate panels use the app.update.url.manual
+        // preference.
+        let labelLink = getSelectedLabelLink();
+        is(labelLink.href, URL_HOST,
+           "The panel's link href should equal the expected value");
+      }
+
+      let buttonPanels = ["downloadAndInstall", "apply"];
+      if (buttonPanels.includes(panelId)) {
+        let buttonEl = getSelectedPanelButton();
+        await BrowserTestUtils.waitForCondition(() =>
+          (aboutDialog.document.activeElement == buttonEl),
+          "The button should receive focus");
+        ok(!buttonEl.disabled, "The button should be enabled");
+        // Don't click the button on the apply panel since this will restart the
+        // application.
+        if (panelId != "apply") {
+          buttonEl.click();
+        }
+      }
+    })();
+  }
+
+  return (async function() {
+    await SpecialPowers.pushPrefEnv({
+      set: [
+        [PREF_APP_UPDATE_SERVICE_ENABLED, false],
+        [PREF_APP_UPDATE_DISABLEDFORTESTING, false],
+        [PREF_APP_UPDATE_URL_MANUAL, URL_HOST],
+      ],
+    });
+    registerCleanupFunction(() => {
+      gEnv.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "");
+      UpdateListener.reset();
+      cleanUpUpdates();
+    });
+
+    gEnv.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "1");
+    setUpdateTimerPrefs();
+    removeUpdateDirsAndFiles();
+
+    await setupTestUpdater();
+
+    let url = URL_HTTP_UPDATE_SJS + "?detailsURL=" + URL_HOST +
+              updateParams + getVersionParams();
+    if (backgroundUpdate) {
+      setUpdateURL(url);
+      if (Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED)) {
+        // Don't wait on the deletion of the continueStaging file
+        continueFileHandler(CONTINUE_STAGING);
+      }
+      gAUS.checkForBackgroundUpdates();
+      await waitForEvent("update-downloaded");
+    } else {
+      url += "&slowUpdateCheck=1&useSlowDownloadMar=1";
+      setUpdateURL(url);
+    }
+
+    aboutDialog = await waitForAboutDialog();
+
+    for (let step of steps) {
+      await processAboutDialogStep(step);
+    }
+
+    aboutDialog.close();
+    await finishTestRestoreUpdaterBackup();
+  })();
+}
--- a/toolkit/mozapps/update/tests/browser/testConstants.js
+++ b/toolkit/mozapps/update/tests/browser/testConstants.js
@@ -1,4 +1,7 @@
 const REL_PATH_DATA = "browser/toolkit/mozapps/update/tests/browser/";
 const URL_HOST = "http://example.com";
-const URL_PATH_UPDATE_XML = "/" + REL_PATH_DATA + "update.sjs";
+const URL_PATH_UPDATE_XML = "/" + REL_PATH_DATA + "app_update.sjs";
 const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML;
+const CONTINUE_CHECK = "continueCheck";
+const CONTINUE_DOWNLOAD = "continueDownload";
+const CONTINUE_STAGING = "continueStaging";
--- a/toolkit/mozapps/update/tests/chrome/chrome.ini
+++ b/toolkit/mozapps/update/tests/chrome/chrome.ini
@@ -1,16 +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/.
 
 [DEFAULT]
 tags = appupdate
 support-files =
   testConstants.js
+  update.sjs
   utils.js
 
 # mochitest-chrome tests must start with "test_" and are executed in sorted
 # order and not in the order specified in the manifest.
 [test_0010_background_basic.xul]
 [test_0011_check_basic.xul]
 [test_0012_check_basic_staging.xul]
 skip-if = asan
copy from toolkit/mozapps/update/tests/data/update.sjs
copy to toolkit/mozapps/update/tests/chrome/update.sjs
--- a/toolkit/mozapps/update/tests/moz.build
+++ b/toolkit/mozapps/update/tests/moz.build
@@ -60,24 +60,22 @@ if CONFIG['OS_ARCH'] == 'WINNT':
     USE_STATIC_LIBS = True
     if CONFIG['CC_TYPE'] in ('clang', 'gcc'):
         WIN32_EXE_LDFLAGS += ['-municode']
 
 TEST_HARNESS_FILES.testing.mochitest.browser.toolkit.mozapps.update.tests.browser += [
     'data/shared.js',
     'data/sharedUpdateXML.js',
     'data/simple.mar',
-    'data/update.sjs',
 ]
 
 TEST_HARNESS_FILES.testing.mochitest.chrome.toolkit.mozapps.update.tests.chrome += [
     'data/shared.js',
     'data/sharedUpdateXML.js',
     'data/simple.mar',
-    'data/update.sjs',
 ]
 
 FINAL_TARGET_FILES += [
     'data/complete.exe',
     'data/complete.mar',
     'data/complete.png',
     'data/complete_log_success_mac',
     'data/complete_log_success_win',
--- a/toolkit/mozapps/update/updater/updater.cpp
+++ b/toolkit/mozapps/update/updater/updater.cpp
@@ -2454,16 +2454,43 @@ static void UpdateThreadFunc(void *param
 
     if (rv == OK && sStagedUpdate) {
 #ifdef TEST_UPDATER
       // The MOZ_TEST_SKIP_UPDATE_STAGE environment variable prevents copying
       // the files in dist/bin in the test updater when staging an update since
       // this can cause tests to timeout.
       if (EnvHasValue("MOZ_TEST_SKIP_UPDATE_STAGE")) {
         rv = OK;
+      } else if (EnvHasValue("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE")) {
+        // The following is to simulate staging so the UI tests have time to
+        // show that the update is being staged.
+        NS_tchar continueFilePath[MAXPATHLEN] = {NS_T('\0')};
+        NS_tsnprintf(continueFilePath,
+                     sizeof(continueFilePath) / sizeof(continueFilePath[0]),
+                     NS_T("%s/continueStaging"), gPatchDirPath);
+        // Use 100 retries for staging requests to lessen the likelihood of
+        // tests intermittently failing on debug builds due to launching the
+        // updater. The total time to wait with the default interval of 100 ms
+        // is approximately 10 seconds. The tests use the same values.
+        const int max_retries = 100;
+        int retries = 1;
+        while (retries++ < max_retries) {
+#ifdef XP_WIN
+          Sleep(100);
+#else
+          usleep(100000);
+#endif
+          // Continue after the continue file exists and it is successfully
+          // removed.
+          if (!NS_taccess(continueFilePath, F_OK) &&
+              !NS_tremove(continueFilePath)) {
+            break;
+          }
+        }
+        rv = OK;
       } else {
         rv = CopyInstallDirToDestDir();
       }
 #else
       rv = CopyInstallDirToDestDir();
 #endif
     }