Bug 1486971 - Test for dynamically change of the prefers-reduced-motion setting on MacOSX. r=froydnj,mstange
authorHiroyuki Ikezoe <hikezoe@mozilla.com>
Sat, 15 Sep 2018 01:00:07 +0000
changeset 494856 67a5acf7363d9dd2c8532967ec1beb0c200abc2c
parent 494855 cea8a92452d58e492b4498e9387b743261146921
child 494857 2ba4d3d517aacb225e1fdcee0cfb1d74f821210f
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfroydnj, mstange
bugs1486971, 1478212
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1486971 - Test for dynamically change of the prefers-reduced-motion setting on MacOSX. r=froydnj,mstange The framework to simulate the setting change works as following; - nsIDOMWindowUtils.setPrefersReducedMotion() calls an IPC function which ends up calling nsChildView::SetPrefersReducedMotion() in the parent process - nsChildView::SetPrefersReducedMotion() sets the given value into nsLookAndFeel::mPrefersReducedMotionCached just like we set the value queried via NSWorkspace.accessibilityDisplayShouldReduceMotion in the parent process and send a notification which is the same notification MacOSX sends when the system setting changed - Normally the cached value is cleared before quering new values since the cache value is stale, but in this case the value is up-to-date one, so nsChildView::SetPrefersReducedMotion() tells that we don't need to clear the cache, and nsIDOMWindowUtils.resetPrefersReducedMotion() resets that state of 'we don't need to clear the cache' There are two test cases with the framework in this commit, one is just setting the value and checking the value queried by window.matchMedia. The other one is receiving 'change' event and checking the value of the event target. Note that to make this test works the patch for bug 1478212 is necessary since the test runs in an iframe. Depends on D5003 Differential Revision: https://phabricator.services.mozilla.com/D5004
dom/base/nsDOMWindowUtils.cpp
dom/interfaces/base/nsIDOMWindowUtils.idl
dom/ipc/PBrowser.ipdl
dom/ipc/TabParent.cpp
dom/ipc/TabParent.h
ipc/ipdl/sync-messages.ini
layout/style/test/mochitest.ini
layout/style/test/test_mq_prefers_reduced_motion_dynamic.html
widget/LookAndFeel.h
widget/PuppetWidget.cpp
widget/PuppetWidget.h
widget/cocoa/nsChildView.h
widget/cocoa/nsChildView.mm
widget/cocoa/nsLookAndFeel.mm
widget/nsIWidget.h
widget/nsXPLookAndFeel.cpp
widget/nsXPLookAndFeel.h
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -4356,16 +4356,38 @@ nsDOMWindowUtils::EnsureDirtyRootFrame()
     return NS_ERROR_FAILURE;
   }
 
   presShell->FrameNeedsReflow(frame, nsIPresShell::eStyleChange,
                               NS_FRAME_IS_DIRTY);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsDOMWindowUtils::SetPrefersReducedMotionOverrideForTest(bool aValue)
+{
+  nsIWidget* widget = GetWidget();
+  if (!widget) {
+    return NS_OK;
+  }
+
+  return widget->SetPrefersReducedMotionOverrideForTest(aValue);
+}
+
+NS_IMETHODIMP
+nsDOMWindowUtils::ResetPrefersReducedMotionOverrideForTest()
+{
+  nsIWidget* widget = GetWidget();
+  if (!widget) {
+    return NS_OK;
+  }
+
+  return widget->ResetPrefersReducedMotionOverrideForTest();
+}
+
 NS_INTERFACE_MAP_BEGIN(nsTranslationNodeList)
   NS_INTERFACE_MAP_ENTRY(nsISupports)
   NS_INTERFACE_MAP_ENTRY(nsITranslationNodeList)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_ADDREF(nsTranslationNodeList)
 NS_IMPL_RELEASE(nsTranslationNodeList)
 
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -1935,16 +1935,29 @@ interface nsIDOMWindowUtils : nsISupport
    */
   bool isCssPropertyRecordedInUseCounter(in ACString aProperty);
 
   /**
    * NOTE: Currently works only on GTK+.
    */
   attribute ACString systemFont;
 
+
+  /**
+   * Simulate the system setting corresponding to 'prefers-reduced-motion'
+   * media queries feature is changed to 'on' or 'off'.
+   *
+   * Currently this function is available only on MacOSX.
+   */
+  void setPrefersReducedMotionOverrideForTest(in boolean aValue);
+  /**
+   * Reset the internal state to be used for above setPrefersReducedMotion.
+   */
+  void resetPrefersReducedMotionOverrideForTest();
+
   // These consts are only for testing purposes.
   const long DEFAULT_MOUSE_POINTER_ID = 0;
   const long DEFAULT_PEN_POINTER_ID   = 1;
   const long DEFAULT_TOUCH_POINTER_ID = 2;
 
   // Match WidgetMouseEventBase::buttonType.
   const long MOUSE_BUTTON_LEFT_BUTTON   = 0;
   const long MOUSE_BUTTON_MIDDLE_BUTTON = 1;
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -574,16 +574,19 @@ parent:
      *
      * @param aFirstPartyURI first party of the tab that is requesting access.
      */
     async ShowCanvasPermissionPrompt(nsCString aFirstPartyURI);
 
     sync SetSystemFont(nsCString aFontName);
     sync GetSystemFont() returns (nsCString retval);
 
+    sync SetPrefersReducedMotionOverrideForTest(bool aValue);
+    sync ResetPrefersReducedMotionOverrideForTest();
+
 child:
     /**
      * Notify the remote browser that it has been Show()n on this
      * side, with the given |visibleRect|.  This message is expected
      * to trigger creation of the remote browser's "widget".
      *
      * |Show()| and |Move()| take IntSizes rather than Rects because
      * content processes always render to a virtual <0, 0> top-left
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -1562,16 +1562,36 @@ TabParent::RecvClearNativeTouchSequence(
   AutoSynthesizedEventResponder responder(this, aObserverId, "cleartouch");
   nsCOMPtr<nsIWidget> widget = GetWidget();
   if (widget) {
     widget->ClearNativeTouchSequence(responder.GetObserver());
   }
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult
+TabParent::RecvSetPrefersReducedMotionOverrideForTest(const bool& aValue)
+{
+  nsCOMPtr<nsIWidget> widget = GetWidget();
+  if (widget) {
+    widget->SetPrefersReducedMotionOverrideForTest(aValue);
+  }
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+TabParent::RecvResetPrefersReducedMotionOverrideForTest()
+{
+  nsCOMPtr<nsIWidget> widget = GetWidget();
+  if (widget) {
+    widget->ResetPrefersReducedMotionOverrideForTest();
+  }
+  return IPC_OK();
+}
+
 void
 TabParent::SendRealKeyEvent(WidgetKeyboardEvent& aEvent)
 {
   if (mIsDestroyed || !mIsReadyToHandleInputEvents) {
     return;
   }
   aEvent.mRefPoint += GetChildProcessOffset();
 
--- a/dom/ipc/TabParent.h
+++ b/dom/ipc/TabParent.h
@@ -412,16 +412,21 @@ public:
   virtual mozilla::ipc::IPCResult
   RecvSynthesizeNativeTouchTap(const LayoutDeviceIntPoint& aPoint,
                                const bool& aLongTap,
                                const uint64_t& aObserverId) override;
 
   virtual mozilla::ipc::IPCResult
   RecvClearNativeTouchSequence(const uint64_t& aObserverId) override;
 
+  virtual mozilla::ipc::IPCResult
+  RecvSetPrefersReducedMotionOverrideForTest(const bool& aValue) override;
+  virtual mozilla::ipc::IPCResult
+  RecvResetPrefersReducedMotionOverrideForTest() override;
+
   void SendMouseEvent(const nsAString& aType, float aX, float aY,
                       int32_t aButton, int32_t aClickCount,
                       int32_t aModifiers, bool aIgnoreRootScrollFrame);
 
   /**
    * The following Send*Event() marks aEvent as posted to remote process if
    * it succeeded.  So, you can check the result with
    * aEvent.HasBeenPostedToRemoteProcess().
--- a/ipc/ipdl/sync-messages.ini
+++ b/ipc/ipdl/sync-messages.ini
@@ -837,16 +837,20 @@ description =
 [PBrowser::DispatchKeyboardEvent]
 description =
 [PBrowser::EnsureLayersConnected]
 description =
 [PBrowser::SetSystemFont]
 description = test only
 [PBrowser::GetSystemFont]
 description = test only
+[PBrowser::SetPrefersReducedMotionOverrideForTest]
+description = test only
+[PBrowser::ResetPrefersReducedMotionOverrideForTest]
+description = test only
 [PContent::SyncMessage]
 description =
 [PContent::CreateChildProcess]
 description =
 [PContent::BridgeToChildProcess]
 description =
 [PContent::OpenRecordReplayChannel]
 description = bug 1475898 this could be async
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -254,16 +254,18 @@ support-files = slow_broken_sheet.sjs sl
 [test_media_queries.html]
 skip-if = android_version == '18' #debug-only failure; timed out #Android 4.3 aws only; bug 1030419
 [test_media_queries_dynamic.html]
 [test_media_queries_dynamic_xbl.html]
 [test_media_query_list.html]
 [test_media_query_serialization.html]
 [test_mq_any_hover_and_any_pointer.html]
 [test_mq_hover_and_pointer.html]
+[test_mq_prefers_reduced_motion_dynamic.html]
+run-if = os == 'mac' # Currently the test works on only MacOSX
 [test_moz_device_pixel_ratio.html]
 [test_namespace_rule.html]
 [test_non_content_accessible_properties.html]
 [test_non_content_accessible_pseudos.html]
 [test_non_content_accessible_values.html]
 [test_of_type_selectors.xhtml]
 [test_overscroll_behavior_pref.html]
 [test_page_parser.html]
new file mode 100644
--- /dev/null
+++ b/layout/style/test/test_mq_prefers_reduced_motion_dynamic.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1486971
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1478519</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="/tests/SimpleTest/AddTask.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1486971">Mozilla Bug 1486971</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<script>
+'use strict';
+
+async function waitForFrame() {
+  return new Promise(resolve => {
+    window.requestAnimationFrame(resolve);
+  });
+}
+
+// Returns a Promise which will be resolved when the 'change' event is received
+// for the given media query string.
+async function promiseForChange(mediaQuery) {
+  return new Promise(resolve => {
+    window.matchMedia(mediaQuery).addEventListener('change', event => {
+      resolve(event.matches);
+    }, { once: true });
+  });
+}
+
+add_task(async () => {
+  SpecialPowers.DOMWindowUtils.setPrefersReducedMotionOverrideForTest(false);
+
+  // Need to wait a frame since MediaQuery changes are asynchronously processed.
+  await waitForFrame();
+
+  ok(!window.matchMedia('(prefers-reduced-motion: reduce)').matches,
+     'Does not matches prefers-reduced-motion: reduced) when the system sets ' +
+     'prefers-reduced-motion false');
+  ok(!window.matchMedia('(prefers-reduced-motion)').matches,
+     'Does not matches (prefers-reduced-motion) when the system sets ' +
+     'prefers-reduced-motion false');
+  ok(window.matchMedia('(prefers-reduced-motion: no-preference)').matches,
+     'Matches (prefers-reduced-motion: no-preference) when the system sets ' +
+     'prefers-reduced-motion false');
+});
+
+add_task(async () => {
+  const reduce = promiseForChange('(prefers-reduced-motion: reduce)');
+  const booleanContext = promiseForChange('(prefers-reduced-motion)');
+  const noPreference = promiseForChange('(prefers-reduced-motion: no-preference)');
+
+  SpecialPowers.DOMWindowUtils.setPrefersReducedMotionOverrideForTest(true);
+
+  const [ reduceResult, booleanContextResult, noPreferenceResult ] =
+    await Promise.all([ reduce, booleanContext, noPreference ]);
+
+  ok(reduceResult,
+     'Matches (prefers-reduced-motion: reduced) when the system sets ' +
+     'prefers-reduced-motion true');
+  ok(booleanContextResult,
+     'Matches (prefers-reduced-motion) when the system sets ' +
+     'prefers-reduced-motion true');
+  ok(!noPreferenceResult,
+     'Does not matches (prefers-reduced-motion: no-preference) when the ' +
+     'system sets prefers-reduced-motion true');
+
+  SpecialPowers.DOMWindowUtils.resetPrefersReducedMotionOverrideForTest();
+});
+</script>
+</body>
+</html>
--- a/widget/LookAndFeel.h
+++ b/widget/LookAndFeel.h
@@ -682,16 +682,21 @@ public:
   static void NativeInit();
 
   /**
    * If the implementation is caching values, these accessors allow the
    * cache to be exported and imported.
    */
   static nsTArray<LookAndFeelInt> GetIntCache();
   static void SetIntCache(const nsTArray<LookAndFeelInt>& aLookAndFeelIntCache);
+  /**
+   * Set a flag indicating whether the cache should be cleared in RefreshImpl()
+   * or not.
+   */
+  static void SetShouldRetainCacheForTest(bool aValue);
 };
 
 } // namespace mozilla
 
 // On the Mac, GetColor(eColorID_TextSelectForeground, color) returns this
 // constant to specify that the foreground color should not be changed
 // (ie. a colored text keeps its colors  when selected).
 // Of course if other plaforms work like the Mac, they can use it too.
--- a/widget/PuppetWidget.cpp
+++ b/widget/PuppetWidget.cpp
@@ -1563,10 +1563,32 @@ PuppetWidget::GetSystemFont(nsCString& a
 {
   if (!mTabChild) {
     return NS_ERROR_FAILURE;
   }
   mTabChild->SendGetSystemFont(&aFontName);
   return NS_OK;
 }
 
+nsresult
+PuppetWidget::SetPrefersReducedMotionOverrideForTest(bool aValue)
+{
+  if (!mTabChild) {
+    return NS_ERROR_FAILURE;
+  }
+
+  mTabChild->SendSetPrefersReducedMotionOverrideForTest(aValue);
+  return NS_OK;
+}
+
+nsresult
+PuppetWidget::ResetPrefersReducedMotionOverrideForTest()
+{
+  if (!mTabChild) {
+    return NS_ERROR_FAILURE;
+  }
+
+  mTabChild->SendResetPrefersReducedMotionOverrideForTest();
+  return NS_OK;
+}
+
 } // namespace widget
 } // namespace mozilla
--- a/widget/PuppetWidget.h
+++ b/widget/PuppetWidget.h
@@ -309,16 +309,19 @@ public:
                  const nsAString& aText,
                  const nsTArray<mozilla::FontRange>& aFontRangeArray,
                  const bool aIsVertical,
                  const LayoutDeviceIntPoint& aPoint) override;
 
   nsresult SetSystemFont(const nsCString& aFontName) override;
   nsresult GetSystemFont(nsCString& aFontName) override;
 
+  nsresult SetPrefersReducedMotionOverrideForTest(bool aValue) override;
+  nsresult ResetPrefersReducedMotionOverrideForTest() override;
+
   // TextEventDispatcherListener
   using nsBaseWidget::NotifyIME;
   NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher,
                        const IMENotification& aNotification) override;
   NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() override;
   NS_IMETHOD_(void) OnRemovedFrom(
                       TextEventDispatcher* aTextEventDispatcher) override;
   NS_IMETHOD_(void) WillDispatchKeyboardEvent(
--- a/widget/cocoa/nsChildView.h
+++ b/widget/cocoa/nsChildView.h
@@ -544,16 +544,19 @@ public:
 
   virtual LayoutDeviceIntPoint GetClientOffset() override;
 
   void DispatchAPZWheelInputEvent(mozilla::InputData& aEvent, bool aCanTriggerSwipe);
   nsEventStatus DispatchAPZInputEvent(mozilla::InputData& aEvent);
 
   void SwipeFinished();
 
+  nsresult SetPrefersReducedMotionOverrideForTest(bool aValue) override;
+  nsresult ResetPrefersReducedMotionOverrideForTest() override;
+
 protected:
   virtual ~nsChildView();
 
   void              ReportMoveEvent();
   void              ReportSizeEvent();
 
   // override to create different kinds of child views. Autoreleases, so
   // caller must retain.
--- a/widget/cocoa/nsChildView.mm
+++ b/widget/cocoa/nsChildView.mm
@@ -3092,16 +3092,52 @@ nsChildView::LookUpDictionary(
     }
   }
 
   [mView showDefinitionForAttributedString:attrStr atPoint:pt];
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
+nsresult
+nsChildView::SetPrefersReducedMotionOverrideForTest(bool aValue)
+{
+  // Tell that the cache value we are going to set isn't cleared via
+  // nsPresContext::ThemeChangedInternal which is called right before
+  // we queue the media feature value change for this prefers-reduced-motion
+  // change.
+  LookAndFeel::SetShouldRetainCacheForTest(true);
+
+  LookAndFeelInt prefersReducedMotion;
+  prefersReducedMotion.id = LookAndFeel::eIntID_PrefersReducedMotion;
+  prefersReducedMotion.value = aValue ? 1 : 0;
+
+  AutoTArray<LookAndFeelInt, 1> lookAndFeelCache;
+  lookAndFeelCache.AppendElement(prefersReducedMotion);
+
+  // If we could have a way to modify
+  // NSWorkspace.accessibilityDisplayShouldReduceMotion, we could use it, but
+  // unfortunately there is no way, so we change the cache value instead as if
+  // it's set in the parent process.
+  LookAndFeel::SetIntCache(lookAndFeelCache);
+
+  [[NSNotificationCenter defaultCenter]
+     postNotificationName: NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
+     object:nil];
+
+  return NS_OK;
+}
+
+nsresult
+nsChildView::ResetPrefersReducedMotionOverrideForTest()
+{
+  LookAndFeel::SetShouldRetainCacheForTest(false);
+  return NS_OK;
+}
+
 #ifdef ACCESSIBILITY
 already_AddRefed<a11y::Accessible>
 nsChildView::GetDocumentAccessible()
 {
   if (!mozilla::a11y::ShouldA11yBeEnabled())
     return nullptr;
 
   // mAccessible might be dead if accessibility was previously disabled and is
--- a/widget/cocoa/nsLookAndFeel.mm
+++ b/widget/cocoa/nsLookAndFeel.mm
@@ -108,16 +108,20 @@ void
 nsLookAndFeel::NativeInit()
 {
   EnsureInit();
 }
 
 void
 nsLookAndFeel::RefreshImpl()
 {
+  if (mShouldRetainCacheForTest) {
+    return;
+  }
+
   nsXPLookAndFeel::RefreshImpl();
 
   // We should only clear the cache if we're in the main browser process.
   // Otherwise, we should wait for the parent to inform us of new values
   // to cache via LookAndFeel::SetIntCache.
   if (XRE_IsParentProcess()) {
     mUseOverlayScrollbarsCached = false;
     mAllowOverlayScrollbarsOverlapCached = false;
--- a/widget/nsIWidget.h
+++ b/widget/nsIWidget.h
@@ -1708,16 +1708,25 @@ class nsIWidget : public nsISupports
     {
       return NS_ERROR_NOT_IMPLEMENTED;
     }
     virtual nsresult GetSystemFont(nsCString& aFontName)
     {
       return NS_ERROR_NOT_IMPLEMENTED;
     }
 
+    virtual nsresult SetPrefersReducedMotionOverrideForTest(bool aValue)
+    {
+      return NS_ERROR_NOT_IMPLEMENTED;
+    }
+    virtual nsresult ResetPrefersReducedMotionOverrideForTest()
+    {
+      return NS_ERROR_NOT_IMPLEMENTED;
+    }
+
 private:
   class LongTapInfo
   {
   public:
     LongTapInfo(int32_t aPointerId, LayoutDeviceIntPoint& aPoint,
                 mozilla::TimeDuration aDuration,
                 nsIObserver* aObserver) :
       mPointerId(aPointerId),
--- a/widget/nsXPLookAndFeel.cpp
+++ b/widget/nsXPLookAndFeel.cpp
@@ -312,17 +312,19 @@ nsXPLookAndFeel::Shutdown()
   if (sShutdown) {
     return;
   }
   sShutdown = true;
   delete sInstance;
   sInstance = nullptr;
 }
 
-nsXPLookAndFeel::nsXPLookAndFeel() : LookAndFeel()
+nsXPLookAndFeel::nsXPLookAndFeel()
+  : LookAndFeel()
+  , mShouldRetainCacheForTest(false)
 {
 }
 
 // static
 void
 nsXPLookAndFeel::IntPrefChanged(nsLookAndFeelIntPref *data)
 {
   if (!data) {
@@ -1058,9 +1060,16 @@ LookAndFeel::GetIntCache()
 
 // static
 void
 LookAndFeel::SetIntCache(const nsTArray<LookAndFeelInt>& aLookAndFeelIntCache)
 {
   return nsLookAndFeel::GetInstance()->SetIntCacheImpl(aLookAndFeelIntCache);
 }
 
+// static
+void
+LookAndFeel::SetShouldRetainCacheForTest(bool aValue)
+{
+  nsLookAndFeel::GetInstance()->SetShouldRetainCacheImplForTest(aValue);
+}
+
 } // namespace mozilla
--- a/widget/nsXPLookAndFeel.h
+++ b/widget/nsXPLookAndFeel.h
@@ -78,16 +78,20 @@ public:
 
   virtual uint32_t GetPasswordMaskDelayImpl()
   {
     return 600;
   }
 
   virtual nsTArray<LookAndFeelInt> GetIntCacheImpl();
   virtual void SetIntCacheImpl(const nsTArray<LookAndFeelInt>& aLookAndFeelIntCache) {}
+  void SetShouldRetainCacheImplForTest(bool aValue)
+  {
+    mShouldRetainCacheForTest = aValue;
+  }
 
   virtual void NativeInit() = 0;
 
 protected:
   nsXPLookAndFeel();
 
   static void IntPrefChanged(nsLookAndFeelIntPref *data);
   static void FloatPrefChanged(nsLookAndFeelFloatPref *data);
@@ -112,11 +116,15 @@ protected:
   static int32_t sCachedColors[LookAndFeel::eColorID_LAST_COLOR];
   static int32_t sCachedColorBits[COLOR_CACHE_SIZE];
   static bool sUseNativeColors;
   static bool sUseStandinsForNativeColors;
   static bool sFindbarModalHighlight;
 
   static nsXPLookAndFeel* sInstance;
   static bool sShutdown;
+
+  // True if we shouldn't clear the cache value in RefreshImpl().
+  // NOTE: This should be used only for testing.
+  bool mShouldRetainCacheForTest;
 };
 
 #endif