Integrate media query evaluation into HTML5 event loop
https://bugs.webkit.org/show_bug.cgi?id=203134
<rdar://problem/56396316>

Reviewed by Antti Koivisto.

Source/WebCore:

Moved the code to call media query listeners to HTML5 event loop's step to update the rendering:
https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering

Tests: fast/media/mq-inverted-colors-live-update-for-listener.html
       fast/media/mq-prefers-reduced-motion-live-update-for-listener.html

* css/MediaQueryMatcher.cpp:
(WebCore::MediaQueryMatcher::evaluateAll): Renamed from styleResolverChanged.
* css/MediaQueryMatcher.h:
* dom/Document.cpp:
(WebCore::Document::updateElementsAffectedByMediaQueries): Split from evaluateMediaQueryList.
This function is still called right after each layout update so that picture element may start
requesting newly selected image resources without having to wait for a rendering update.
But this function will no longer execute arbitrary scripts.
(WebCore::Document::evaluateMediaQueriesAndReportChanges): Split from evaluateMediaQueryList.
Evaluates media query listeners.
* dom/Document.h:
* inspector/agents/InspectorPageAgent.cpp:
(WebCore::InspectorPageAgent::setEmulatedMedia): Force the evaluation of media queries for now
but this code should really be scheduling a rendering update instead so added a FIXME.
* page/Frame.cpp:
(WebCore::Frame::setPrinting): Evaluate media queries. We should consider invoking the full
algorithm to update the rendering here. e.g. intersection observer may add more contents.
* page/Page.cpp:
(WebCore::Page::updateRendering): Call evaluateMediaQueriesAndReportChanges.

LayoutTests:

Added tests for listening to accessiblity related media queries without having any style rules
get affected by those media queries so that we can catch any future regressions. For now,
changing accessiblity settings seem to always schedule a rendering update so there is nothing to do
when these accessibility settings do change.

* fast/media/media-query-list-07.html: Fixed the test to be compatible with new behavior.
* fast/media/mq-inverted-colors-live-update-for-listener-expected.txt: Added.
* fast/media/mq-inverted-colors-live-update-for-listener.html: Added.
* fast/media/mq-prefers-reduced-motion-live-update-for-listener-expected.txt: Added.
* fast/media/mq-prefers-reduced-motion-live-update-for-listener.html: Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251322 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 21c8e27..9f3365b 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,22 @@
+2019-10-19  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Integrate media query evaluation into HTML5 event loop
+        https://bugs.webkit.org/show_bug.cgi?id=203134
+        <rdar://problem/56396316>
+
+        Reviewed by Antti Koivisto.
+
+        Added tests for listening to accessiblity related media queries without having any style rules
+        get affected by those media queries so that we can catch any future regressions. For now,
+        changing accessiblity settings seem to always schedule a rendering update so there is nothing to do
+        when these accessibility settings do change.
+
+        * fast/media/media-query-list-07.html: Fixed the test to be compatible with new behavior.
+        * fast/media/mq-inverted-colors-live-update-for-listener-expected.txt: Added.
+        * fast/media/mq-inverted-colors-live-update-for-listener.html: Added.
+        * fast/media/mq-prefers-reduced-motion-live-update-for-listener-expected.txt: Added.
+        * fast/media/mq-prefers-reduced-motion-live-update-for-listener.html: Added.
+
 2019-10-18  Said Abou-Hallawa  <sabouhallawa@apple.com>
 
         [SVG2]: Remove the SVGExternalResourcesRequired interface
diff --git a/LayoutTests/fast/media/media-query-list-07.html b/LayoutTests/fast/media/media-query-list-07.html
index eb5e4e2..40df75f 100644
--- a/LayoutTests/fast/media/media-query-list-07.html
+++ b/LayoutTests/fast/media/media-query-list-07.html
@@ -25,6 +25,8 @@
         if (!window.testRunner)
             return;
 
+        testRunner.waitUntilDone();
+
         var query = window.matchMedia("screen");
 
         query.addListener(callback1);
@@ -49,6 +51,7 @@
         query.addListener(callback2);
 
         window.internals.settings.setMediaTypeOverride("handheld");
+        requestAnimationFrame(() => testRunner.notifyDone());
     }
 
 </script>
diff --git a/LayoutTests/fast/media/mq-inverted-colors-live-update-for-listener-expected.txt b/LayoutTests/fast/media/mq-inverted-colors-live-update-for-listener-expected.txt
new file mode 100644
index 0000000..fe74e9e
--- /dev/null
+++ b/LayoutTests/fast/media/mq-inverted-colors-live-update-for-listener-expected.txt
@@ -0,0 +1,11 @@
+This tests listeners on MediaQueryList get called when inverted color is turned on when there are no relevant style rules
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS didCallListener is true
+PASS matchMedia("(inverted-colors)").matches is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/media/mq-inverted-colors-live-update-for-listener.html b/LayoutTests/fast/media/mq-inverted-colors-live-update-for-listener.html
new file mode 100644
index 0000000..fd64097
--- /dev/null
+++ b/LayoutTests/fast/media/mq-inverted-colors-live-update-for-listener.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../../resources/js-test.js"></script>
+<script>
+
+description('This tests listeners on MediaQueryList get called when inverted color is turned on when there are no relevant style rules');
+
+jsTestIsAsync = true;
+
+if (window.testRunner)
+    testRunner.waitUntilDone();
+
+let didCallListener = false;
+let timer;
+
+window.onload = () => {
+    window.matchMedia('(inverted-colors)').addListener((e) => {
+        didCallListener = true;
+        finalize();
+    });
+
+    if (!window.internals || !testRunner.runUIScript) {
+        testFailed('This test requires runUIScript');
+        finishJSTest();
+        return;
+    }
+
+    window.internals.settings.forcedColorsAreInvertedAccessibilityValue = "on";
+    testRunner.runUIScript(`
+        uiController.simulateAccessibilitySettingsChangeNotification(() => {
+            uiController.uiScriptComplete("Done");
+        })`, () => {
+        timer = setTimeout(finalize, 1000);
+    });
+}
+
+function finalize() {
+    if (timer)
+        clearTimeout(timer);
+
+    shouldBeTrue('didCallListener');
+    shouldBeTrue('matchMedia("(inverted-colors)").matches');
+    finishJSTest();
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/fast/media/mq-prefers-reduced-motion-live-update-for-listener-expected.txt b/LayoutTests/fast/media/mq-prefers-reduced-motion-live-update-for-listener-expected.txt
new file mode 100644
index 0000000..514a65d
--- /dev/null
+++ b/LayoutTests/fast/media/mq-prefers-reduced-motion-live-update-for-listener-expected.txt
@@ -0,0 +1,11 @@
+This tests listeners on MediaQueryList get called when inverted color is turned on when there are no relevant style rules
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS didCallListener is true
+PASS matchMedia("(prefers-reduced-motion)").matches is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/media/mq-prefers-reduced-motion-live-update-for-listener.html b/LayoutTests/fast/media/mq-prefers-reduced-motion-live-update-for-listener.html
new file mode 100644
index 0000000..471c31e
--- /dev/null
+++ b/LayoutTests/fast/media/mq-prefers-reduced-motion-live-update-for-listener.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../../resources/js-test.js"></script>
+<script>
+
+description('This tests listeners on MediaQueryList get called when inverted color is turned on when there are no relevant style rules');
+
+jsTestIsAsync = true;
+
+if (window.testRunner)
+    testRunner.waitUntilDone();
+
+let didCallListener = false;
+let timer;
+
+window.onload = () => {
+    window.matchMedia('(prefers-reduced-motion)').addListener((e) => {
+        didCallListener = true;
+        finalize();
+    });
+
+    if (!window.internals || !testRunner.runUIScript) {
+        testFailed('This test requires runUIScript');
+        finishJSTest();
+        return;
+    }
+
+    window.internals.settings.forcedPrefersReducedMotionAccessibilityValue = "on";
+    testRunner.runUIScript(`
+        uiController.simulateAccessibilitySettingsChangeNotification(() => {
+            uiController.uiScriptComplete("Done");
+        })`, () => {
+        timer = setTimeout(finalize, 1000);
+    });
+}
+
+function finalize() {
+    if (timer)
+        clearTimeout(timer);
+
+    shouldBeTrue('didCallListener');
+    shouldBeTrue('matchMedia("(prefers-reduced-motion)").matches');
+    finishJSTest();
+}
+
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 5ff25ab..da06e7c 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,37 @@
+2019-10-19  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Integrate media query evaluation into HTML5 event loop
+        https://bugs.webkit.org/show_bug.cgi?id=203134
+        <rdar://problem/56396316>
+
+        Reviewed by Antti Koivisto.
+
+        Moved the code to call media query listeners to HTML5 event loop's step to update the rendering:
+        https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering
+
+        Tests: fast/media/mq-inverted-colors-live-update-for-listener.html
+               fast/media/mq-prefers-reduced-motion-live-update-for-listener.html
+
+        * css/MediaQueryMatcher.cpp:
+        (WebCore::MediaQueryMatcher::evaluateAll): Renamed from styleResolverChanged.
+        * css/MediaQueryMatcher.h:
+        * dom/Document.cpp:
+        (WebCore::Document::updateElementsAffectedByMediaQueries): Split from evaluateMediaQueryList.
+        This function is still called right after each layout update so that picture element may start
+        requesting newly selected image resources without having to wait for a rendering update.
+        But this function will no longer execute arbitrary scripts.
+        (WebCore::Document::evaluateMediaQueriesAndReportChanges): Split from evaluateMediaQueryList.
+        Evaluates media query listeners.
+        * dom/Document.h:
+        * inspector/agents/InspectorPageAgent.cpp:
+        (WebCore::InspectorPageAgent::setEmulatedMedia): Force the evaluation of media queries for now
+        but this code should really be scheduling a rendering update instead so added a FIXME.
+        * page/Frame.cpp:
+        (WebCore::Frame::setPrinting): Evaluate media queries. We should consider invoking the full
+        algorithm to update the rendering here. e.g. intersection observer may add more contents.
+        * page/Page.cpp:
+        (WebCore::Page::updateRendering): Call evaluateMediaQueriesAndReportChanges.
+
 2019-10-18  Tim Horton  <timothy_horton@apple.com>
 
         macCatalyst: Cursor should send mouse events, not touch events
diff --git a/Source/WebCore/css/MediaQueryMatcher.cpp b/Source/WebCore/css/MediaQueryMatcher.cpp
index 52a5589..cd2cefc 100644
--- a/Source/WebCore/css/MediaQueryMatcher.cpp
+++ b/Source/WebCore/css/MediaQueryMatcher.cpp
@@ -109,7 +109,7 @@
     });
 }
 
-void MediaQueryMatcher::styleResolverChanged()
+void MediaQueryMatcher::evaluateAll()
 {
     ASSERT(m_document);
 
diff --git a/Source/WebCore/css/MediaQueryMatcher.h b/Source/WebCore/css/MediaQueryMatcher.h
index 033afb0..99f8efa 100644
--- a/Source/WebCore/css/MediaQueryMatcher.h
+++ b/Source/WebCore/css/MediaQueryMatcher.h
@@ -53,7 +53,7 @@
 
     unsigned evaluationRound() const { return m_evaluationRound; }
 
-    void styleResolverChanged();
+    void evaluateAll();
 
     bool evaluate(const MediaQuerySet&);
 
diff --git a/Source/WebCore/dom/Document.cpp b/Source/WebCore/dom/Document.cpp
index 4b93768..8855786 100644
--- a/Source/WebCore/dom/Document.cpp
+++ b/Source/WebCore/dom/Document.cpp
@@ -3899,15 +3899,21 @@
     return *m_styleSheetList;
 }
 
-void Document::evaluateMediaQueryList()
+void Document::updateElementsAffectedByMediaQueries()
 {
-    if (m_mediaQueryMatcher)
-        m_mediaQueryMatcher->styleResolverChanged();
-    
+    ScriptDisallowedScope::InMainThread scriptDisallowedScope;
     checkViewportDependentPictures();
     checkAppearanceDependentPictures();
 }
 
+void Document::evaluateMediaQueriesAndReportChanges()
+{
+    if (!m_mediaQueryMatcher)
+        return;
+
+    m_mediaQueryMatcher->evaluateAll();
+}
+
 void Document::checkViewportDependentPictures()
 {
     Vector<HTMLPictureElement*, 16> changedPictures;
diff --git a/Source/WebCore/dom/Document.h b/Source/WebCore/dom/Document.h
index 00568a9..a65dd4b 100644
--- a/Source/WebCore/dom/Document.h
+++ b/Source/WebCore/dom/Document.h
@@ -555,7 +555,8 @@
     bool gotoAnchorNeededAfterStylesheetsLoad() { return m_gotoAnchorNeededAfterStylesheetsLoad; }
     void setGotoAnchorNeededAfterStylesheetsLoad(bool b) { m_gotoAnchorNeededAfterStylesheetsLoad = b; }
 
-    void evaluateMediaQueryList();
+    void updateElementsAffectedByMediaQueries();
+    void evaluateMediaQueriesAndReportChanges();
 
     FormController& formController();
     Vector<String> formElementsState() const;
diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp
index 9fedf31..b8ab9b6 100644
--- a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp
+++ b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp
@@ -872,10 +872,15 @@
 
     m_emulatedMedia = media;
 
+    // FIXME: Schedule a rendering update instead of synchronously updating the layout.
     m_inspectedPage.updateStyleAfterChangeInEnvironment();
 
-    if (auto* document = m_inspectedPage.mainFrame().document())
-        document->updateLayout();
+    auto document = makeRefPtr(m_inspectedPage.mainFrame().document());
+    if (!document)
+        return;
+
+    document->updateLayout();
+    document->evaluateMediaQueriesAndReportChanges();
 }
 
 void InspectorPageAgent::setForcedAppearance(ErrorString&, const String& appearance)
diff --git a/Source/WebCore/page/Frame.cpp b/Source/WebCore/page/Frame.cpp
index 6f077c4..f5a5c9d 100644
--- a/Source/WebCore/page/Frame.cpp
+++ b/Source/WebCore/page/Frame.cpp
@@ -560,7 +560,9 @@
     auto& frameView = *view();
     frameView.adjustMediaTypeForPrinting(printing);
 
+    // FIXME: Consider invoking Page::updateRendering or an equivalent.
     m_doc->styleScope().didChangeStyleSheetEnvironment();
+    m_doc->evaluateMediaQueriesAndReportChanges();
     if (shouldUsePrintingLayout())
         frameView.forceLayoutForPagination(pageSize, originalPageSize, maximumShrinkRatio, shouldAdjustViewSize);
     else {
diff --git a/Source/WebCore/page/FrameViewLayoutContext.cpp b/Source/WebCore/page/FrameViewLayoutContext.cpp
index 1c9e315..1e5cb13 100644
--- a/Source/WebCore/page/FrameViewLayoutContext.cpp
+++ b/Source/WebCore/page/FrameViewLayoutContext.cpp
@@ -519,7 +519,7 @@
     // Viewport-dependent media queries may cause us to need completely different style information.
     document.styleScope().evaluateMediaQueriesForViewportChange();
 
-    document.evaluateMediaQueryList();
+    document.updateElementsAffectedByMediaQueries();
     // If there is any pagination to apply, it will affect the RenderView's style, so we should
     // take care of that now.
     view().applyPaginationToViewport();
diff --git a/Source/WebCore/page/Page.cpp b/Source/WebCore/page/Page.cpp
index 161b8a2..9093f8c 100644
--- a/Source/WebCore/page/Page.cpp
+++ b/Source/WebCore/page/Page.cpp
@@ -1301,7 +1301,8 @@
 
     // FIXME: Run the scroll steps
 
-    // FIXME: Evaluate media queries and report changes.
+    for (auto& document : collectDocuments())
+        document->evaluateMediaQueriesAndReportChanges();
 
     Vector<Ref<Document>> documents = collectDocuments(); // The requestAnimationFrame callbacks may change the frame hierarchy of the page
     for (auto& document : documents) {
@@ -2663,7 +2664,7 @@
     for (Frame* frame = &mainFrame(); frame; frame = frame->tree().traverseNext()) {
         if (auto* document = frame->document()) {
             document->styleScope().evaluateMediaQueriesForAccessibilitySettingsChange();
-            document->evaluateMediaQueryList();
+            document->updateElementsAffectedByMediaQueries();
         }
     }
 }
@@ -2677,7 +2678,7 @@
 
         document->styleScope().didChangeStyleSheetEnvironment();
         document->styleScope().evaluateMediaQueriesForAppearanceChange();
-        document->evaluateMediaQueryList();
+        document->updateElementsAffectedByMediaQueries();
     }
 }