[WK2] EditorState updates should be rolled into the layer update lifecycle when possible
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 23 Aug 2017 03:15:38 +0000 (03:15 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 23 Aug 2017 03:15:38 +0000 (03:15 +0000)
https://bugs.webkit.org/show_bug.cgi?id=175370
<rdar://problem/33799806>

Reviewed by Ryosuke Niwa.

Source/WebCore:

Remove didChangeSelectionAndUpdateLayout -- EditorState updates that are scheduled due to missing post-layout
data will now be scheduled for the next presentation update. Additionally, add editor client hooks to notify the
WebKit layer when we've updated the current composition. See WebKit ChangeLog for more details. This patch
adjusts and rebaselines existing layout tests.

* editing/Editor.cpp:
(WebCore::SetCompositionScope::SetCompositionScope):
(WebCore::SetCompositionScope::~SetCompositionScope):

Introduce a helper RAII class to ensure that we ignore selection changes during the scope of
Editor::setComposition and call out to the client with WebEditorClient::didUpdateComposition afterwards. This
also maintains a UserTypingGestureIndicator over its lifetime, so we don't additionally need to create a
UserTypingGestureIndicator in Editor::setComposition.

(WebCore::Editor::setComposition):
* editing/FrameSelection.cpp:
(WebCore::FrameSelection::setSelection):
(WebCore::FrameSelection::updateAndRevealSelection):
(WebCore::FrameSelection::setSelectedRange):
* editing/FrameSelection.h:
(WebCore::FrameSelection::defaultSetSelectionOptions):

Plumb state about whether or not the selection change was triggered by the user to FrameSelection::setSelection,
and if so, notify the editing client. A separate SetSelectionOptions flag is used here instead of
RevealSelection to avoid calling out to the client in places where we want to reveal the selection regardless of
whether or not the selection is user triggered.

* loader/EmptyClients.cpp:
* page/EditorClient.h:

Source/WebKit:

See per-method comments for more detail. WebPage::didChangeSelection now schedules EditorState updates to be sent
during the next layer tree transaction rather than sending them synchronously. To ensure that iOS and Mac continue
to behave correctly w.r.t. EditorState updates, we immediately dispatch EditorStates in the following cases:
- After the composition changes, is confirmed, or is canceled.
- After an edit command is executed.
- After ending user-triggered selection changes.

* Shared/mac/RemoteLayerTreeTransaction.h:
(WebKit::RemoteLayerTreeTransaction::hasEditorState const):
(WebKit::RemoteLayerTreeTransaction::editorState const):
(WebKit::RemoteLayerTreeTransaction::setEditorState):

Attaches an optional EditorState to the RemoteLayerTreeTransaction. This EditorState is computed and sent over
when setting up the transaction in WebPage, if something previously scheduled an EditorState update.

* Shared/mac/RemoteLayerTreeTransaction.mm:
(WebKit::RemoteLayerTreeTransaction::encode const):
(WebKit::RemoteLayerTreeTransaction::decode):

Add coder support for sending over a layer tree transaction's EditorState.

* UIProcess/API/Cocoa/WKViewPrivate.h:
* UIProcess/API/mac/WKView.mm:
(-[WKView _doAfterNextPresentationUpdate:]):

Add _doAfterNextPresentationUpdate to WKView (used in TestWebKitAPI -- refer to
WebKitAgnosticTest::waitForNextPresentationUpdate).

* UIProcess/DrawingAreaProxy.h:
(WebKit::DrawingAreaProxy::dispatchPresentationCallbacksAfterFlushingLayers):
* UIProcess/DrawingAreaProxy.messages.in:

Add a new IPC messages, DispatchPresentationCallbacksAfterFlushingLayers, to invoke in-flight presentation
callbacks in the UI process following a layer flush in the web process.

* UIProcess/WebPageProxy.h:
* UIProcess/mac/RemoteLayerTreeDrawingAreaProxy.mm:
(WebKit::RemoteLayerTreeDrawingAreaProxy::commitLayerTree):
* UIProcess/mac/TiledCoreAnimationDrawingAreaProxy.h:
* UIProcess/mac/TiledCoreAnimationDrawingAreaProxy.mm:
(WebKit::TiledCoreAnimationDrawingAreaProxy::~TiledCoreAnimationDrawingAreaProxy):
(WebKit::TiledCoreAnimationDrawingAreaProxy::dispatchAfterEnsuringDrawing):
(WebKit::TiledCoreAnimationDrawingAreaProxy::dispatchPresentationCallbacksAfterFlushingLayers):

Run all pending _doAfterNextPresentationUpdate callbacks.

* WebProcess/WebCoreSupport/WebEditorClient.cpp:
(WebKit::WebEditorClient::didApplyStyle):
(WebKit::WebEditorClient::respondToChangedContents):
(WebKit::WebEditorClient::didEndUserTriggeredSelectionChanges):
(WebKit::WebEditorClient::didUpdateComposition):

Forward editor client calls to the WebPage.

(WebKit::WebEditorClient::didChangeSelectionAndUpdateLayout): Deleted.
* WebProcess/WebCoreSupport/WebEditorClient.h:
* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::editorState const):
(WebKit::WebPage::updateEditorStateAfterLayoutIfEditabilityChanged):
(WebKit::WebPage::willCommitLayerTree):
(WebKit::WebPage::didApplyStyle):

Allow style application to immediately trigger EditorState updates, if we're not currently ignoring selection
changes in the Editor.

(WebKit::WebPage::didChangeContents):

Allow applying top-level edit commands to immediately trigger EditorState updates, if we're not currently
ignoring selection changes in the Editor.

(WebKit::WebPage::didChangeSelection):
(WebKit::WebPage::didUpdateComposition):
(WebKit::WebPage::didEndUserTriggeredSelectionChanges):
(WebKit::WebPage::discardedComposition):
(WebKit::WebPage::canceledComposition):

When handling composition updates, always send an EditorState to the UI process. Unlike other cases, IME
requires immediate EditorState data, so we need to be explicit here in sending updates right away.

(WebKit::WebPage::sendEditorStateUpdate):
(WebKit::WebPage::sendPartialEditorStateAndSchedulePostLayoutUpdate):
(WebKit::WebPage::flushPendingEditorStateUpdate):

Helper methods to schedule an EditorState update to be sent upon the next layer tree update, or flush any
pending EditorState update that has been scheduled. The private, more aggressive variant of this is
sendEditorStateUpdate, which ignores whether or not there was already an EditorState update scheduled, and sends
one anyways (this still fulfills any EditorState update that was previously scheduled).

These helper methods are treated as no-ops when invoked while ignoring selection changes. This is to prevent
temporary selection state and editor commands during operations such as text indicator snapshotting from pushing
bogus information about transient editor states to the UI process.

(WebKit::WebPage::sendPostLayoutEditorStateIfNeeded): Deleted.
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::WebPage::platformEditorState const):
(WebKit::WebPage::executeEditCommandWithCallback):
(WebKit::selectionIsInsideFixedPositionContainer):
(WebKit::WebPage::updateVisibleContentRects):

Fix a hack that was computing an EditorState to figure out whether the current selection starts or ends in a
fixed position container. Factors out relevant logic into a separate helper, and also schedules an EditorState
update instead of immediately computing it.

* WebProcess/WebPage/mac/TiledCoreAnimationDrawingArea.h:
* WebProcess/WebPage/mac/TiledCoreAnimationDrawingArea.mm:
(WebKit::TiledCoreAnimationDrawingArea::addTransactionCallbackID):

Add support for registering and dispatching presentation callbacks that hook into the layer flush lifecycle,
using the tiled CA drawing area. These are used by Mac LayoutTests and API tests that need to wait until the
next flush before checking for state that depends on EditorState updates in the UI process.

(WebKit::TiledCoreAnimationDrawingArea::flushLayers):

Tell the WebPage to flush any pending EditorState updates.

* WebProcess/WebPage/mac/WebPageMac.mm:
(WebKit::WebPage::platformEditorState const):

Source/WebKitLegacy/mac:

Adjust WebEditorClient for interface changes.

* WebCoreSupport/WebEditorClient.h:

Source/WebKitLegacy/win:

Adjust WebEditorClient for interface changes.

* WebCoreSupport/WebEditorClient.h:

Tools:

Tweaks API tests that involve editing to wait for a presentation update before checking against UI process-side
information sent via EditorState updates. This allows any EditorState update scheduled by the test to propagate
to the UI process.

* TestWebKitAPI/Tests/WebKit2Cocoa/WKWebViewCandidateTests.mm:
(-[CandidateTestWebView typeString:inputMessage:]):
(+[CandidateTestWebView setUpWithFrame:testPage:]):
* TestWebKitAPI/Tests/WebKit2Cocoa/WKWebViewTextInput.mm:
* TestWebKitAPI/Tests/mac/AcceptsFirstMouse.mm:
(TestWebKitAPI::AcceptsFirstMouse::runTest):
* TestWebKitAPI/Tests/mac/WKWebViewMacEditingTests.mm:
* TestWebKitAPI/cocoa/TestWKWebView.h:
* TestWebKitAPI/cocoa/TestWKWebView.mm:
(-[TestWKWebView waitForNextPresentationUpdate]):

Add a new helper method to spin until the next presentation update.

* TestWebKitAPI/mac/WebKitAgnosticTest.h:
* TestWebKitAPI/mac/WebKitAgnosticTest.mm:
(TestWebKitAPI::WebKitAgnosticTest::waitForNextPresentationUpdate):

LayoutTests:

Rebaseline and adjust LayoutTests.

* editing/caret/ios/absolute-caret-position-after-scroll-expected.txt:
* editing/caret/ios/absolute-caret-position-after-scroll.html:
* editing/caret/ios/fixed-caret-position-after-scroll-expected.txt:
* editing/caret/ios/fixed-caret-position-after-scroll.html:
* editing/secure-input/password-input-changed-type.html:
* editing/secure-input/password-input-focusing.html:
* editing/secure-input/removed-password-input.html:
* editing/secure-input/reset-state-on-navigation.html:
* editing/selection/character-granularity-rect.html:

Delay checking for secure input state and caret rects until after the next presentation update.

* editing/selection/ios/absolute-selection-after-scroll-expected.txt:
* editing/selection/ios/absolute-selection-after-scroll.html:
* editing/selection/ios/fixed-selection-after-scroll-expected.txt:
* editing/selection/ios/fixed-selection-after-scroll.html:

Refactor and simplify these tests. These tests are not run on the OpenSource bots, since they depend on long
press and tap gestures.

* platform/ios-wk2/editing/inserting/insert-div-024-expected.txt:
* platform/ios-wk2/editing/inserting/insert-div-026-expected.txt:
* platform/ios-wk2/editing/style/5084241-expected.txt:

Rebaselines these tests, removing an anonymous RenderBlock inserted as a result of inserting and removing a
dummy span in order to compute a RenderStyle in WebPage::editorState. This is because editorState is no longer
invoked immediately on page load; https://bugs.webkit.org/show_bug.cgi?id=175116 tracks preventing this render
tree thrashing altogether.

* platform/mac-wk2/TestExpectations:
* platform/mac-wk2/editing/style/unbold-in-bold-expected.txt:
* resources/ui-helper.js:

Introduce new UIHelper functions.

(window.UIHelper.ensurePresentationUpdate.return.new.Promise):
(window.UIHelper.ensurePresentationUpdate):

Returns a Promise, resolved after the next presentation update.

(window.UIHelper.activateAndWaitForInputSessionAt.return.new.Promise.):
(window.UIHelper.activateAndWaitForInputSessionAt.return.new.Promise):
(window.UIHelper.activateAndWaitForInputSessionAt):

Returns a Promise, resolved after tapping at the given location and waiting for the keyboard to appear on iOS.

(window.UIHelper.getUICaretRect.return.new.Promise.):
(window.UIHelper.getUICaretRect.return.new.Promise):
(window.UIHelper.getUICaretRect):
(window.UIHelper.getUISelectionRects.return.new.Promise.):
(window.UIHelper.getUISelectionRects.return.new.Promise):
(window.UIHelper.getUISelectionRects):

Helpers to fetch selection and caret rect information in the UI process.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@221064 268f45cc-cd09-0410-ab3c-d52691b4dbfc

57 files changed:
LayoutTests/ChangeLog
LayoutTests/editing/caret/ios/absolute-caret-position-after-scroll-expected.txt
LayoutTests/editing/caret/ios/absolute-caret-position-after-scroll.html
LayoutTests/editing/caret/ios/fixed-caret-position-after-scroll-expected.txt
LayoutTests/editing/caret/ios/fixed-caret-position-after-scroll.html
LayoutTests/editing/secure-input/password-input-changed-type.html
LayoutTests/editing/secure-input/password-input-focusing.html
LayoutTests/editing/secure-input/removed-password-input.html
LayoutTests/editing/secure-input/reset-state-on-navigation.html
LayoutTests/editing/selection/ios/absolute-selection-after-scroll-expected.txt
LayoutTests/editing/selection/ios/absolute-selection-after-scroll.html
LayoutTests/editing/selection/ios/fixed-selection-after-scroll-expected.txt
LayoutTests/editing/selection/ios/fixed-selection-after-scroll.html
LayoutTests/platform/ios-wk2/editing/inserting/insert-div-024-expected.txt
LayoutTests/platform/ios-wk2/editing/inserting/insert-div-026-expected.txt
LayoutTests/platform/ios-wk2/editing/style/5084241-expected.txt
LayoutTests/platform/mac-wk2/TestExpectations
LayoutTests/platform/mac-wk2/editing/style/unbold-in-bold-expected.txt
LayoutTests/resources/ui-helper.js
Source/WebCore/ChangeLog
Source/WebCore/editing/Editor.cpp
Source/WebCore/editing/FrameSelection.cpp
Source/WebCore/editing/FrameSelection.h
Source/WebCore/loader/EmptyClients.cpp
Source/WebCore/page/EditorClient.h
Source/WebKit/ChangeLog
Source/WebKit/Shared/mac/RemoteLayerTreeTransaction.h
Source/WebKit/Shared/mac/RemoteLayerTreeTransaction.mm
Source/WebKit/UIProcess/API/Cocoa/WKViewPrivate.h
Source/WebKit/UIProcess/API/mac/WKView.mm
Source/WebKit/UIProcess/DrawingAreaProxy.h
Source/WebKit/UIProcess/DrawingAreaProxy.messages.in
Source/WebKit/UIProcess/WebPageProxy.h
Source/WebKit/UIProcess/mac/RemoteLayerTreeDrawingAreaProxy.mm
Source/WebKit/UIProcess/mac/TiledCoreAnimationDrawingAreaProxy.h
Source/WebKit/UIProcess/mac/TiledCoreAnimationDrawingAreaProxy.mm
Source/WebKit/WebProcess/WebCoreSupport/WebEditorClient.cpp
Source/WebKit/WebProcess/WebCoreSupport/WebEditorClient.h
Source/WebKit/WebProcess/WebPage/WebPage.cpp
Source/WebKit/WebProcess/WebPage/WebPage.h
Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
Source/WebKit/WebProcess/WebPage/mac/TiledCoreAnimationDrawingArea.h
Source/WebKit/WebProcess/WebPage/mac/TiledCoreAnimationDrawingArea.mm
Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm
Source/WebKitLegacy/mac/ChangeLog
Source/WebKitLegacy/mac/WebCoreSupport/WebEditorClient.h
Source/WebKitLegacy/win/ChangeLog
Source/WebKitLegacy/win/WebCoreSupport/WebEditorClient.h
Tools/ChangeLog
Tools/TestWebKitAPI/Tests/WebKit2Cocoa/WKWebViewCandidateTests.mm
Tools/TestWebKitAPI/Tests/WebKit2Cocoa/WKWebViewTextInput.mm
Tools/TestWebKitAPI/Tests/mac/AcceptsFirstMouse.mm
Tools/TestWebKitAPI/Tests/mac/WKWebViewMacEditingTests.mm
Tools/TestWebKitAPI/cocoa/TestWKWebView.h
Tools/TestWebKitAPI/cocoa/TestWKWebView.mm
Tools/TestWebKitAPI/mac/WebKitAgnosticTest.h
Tools/TestWebKitAPI/mac/WebKitAgnosticTest.mm

index c1b2133..ff89abf 100644 (file)
@@ -1,3 +1,68 @@
+2017-08-22  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+        https://bugs.webkit.org/show_bug.cgi?id=175370
+        <rdar://problem/33799806>
+
+        Reviewed by Ryosuke Niwa.
+
+        Rebaseline and adjust LayoutTests.
+
+        * editing/caret/ios/absolute-caret-position-after-scroll-expected.txt:
+        * editing/caret/ios/absolute-caret-position-after-scroll.html:
+        * editing/caret/ios/fixed-caret-position-after-scroll-expected.txt:
+        * editing/caret/ios/fixed-caret-position-after-scroll.html:
+        * editing/secure-input/password-input-changed-type.html:
+        * editing/secure-input/password-input-focusing.html:
+        * editing/secure-input/removed-password-input.html:
+        * editing/secure-input/reset-state-on-navigation.html:
+        * editing/selection/character-granularity-rect.html:
+
+        Delay checking for secure input state and caret rects until after the next presentation update.
+
+        * editing/selection/ios/absolute-selection-after-scroll-expected.txt:
+        * editing/selection/ios/absolute-selection-after-scroll.html:
+        * editing/selection/ios/fixed-selection-after-scroll-expected.txt:
+        * editing/selection/ios/fixed-selection-after-scroll.html:
+
+        Refactor and simplify these tests. These tests are not run on the OpenSource bots, since they depend on long
+        press and tap gestures.
+
+        * platform/ios-wk2/editing/inserting/insert-div-024-expected.txt:
+        * platform/ios-wk2/editing/inserting/insert-div-026-expected.txt:
+        * platform/ios-wk2/editing/style/5084241-expected.txt:
+
+        Rebaselines these tests, removing an anonymous RenderBlock inserted as a result of inserting and removing a
+        dummy span in order to compute a RenderStyle in WebPage::editorState. This is because editorState is no longer
+        invoked immediately on page load; https://bugs.webkit.org/show_bug.cgi?id=175116 tracks preventing this render
+        tree thrashing altogether.
+
+        * platform/mac-wk2/TestExpectations:
+        * platform/mac-wk2/editing/style/unbold-in-bold-expected.txt:
+        * resources/ui-helper.js:
+
+        Introduce new UIHelper functions.
+
+        (window.UIHelper.ensurePresentationUpdate.return.new.Promise):
+        (window.UIHelper.ensurePresentationUpdate):
+
+        Returns a Promise, resolved after the next presentation update.
+
+        (window.UIHelper.activateAndWaitForInputSessionAt.return.new.Promise.):
+        (window.UIHelper.activateAndWaitForInputSessionAt.return.new.Promise):
+        (window.UIHelper.activateAndWaitForInputSessionAt):
+
+        Returns a Promise, resolved after tapping at the given location and waiting for the keyboard to appear on iOS.
+
+        (window.UIHelper.getUICaretRect.return.new.Promise.):
+        (window.UIHelper.getUICaretRect.return.new.Promise):
+        (window.UIHelper.getUICaretRect):
+        (window.UIHelper.getUISelectionRects.return.new.Promise.):
+        (window.UIHelper.getUISelectionRects.return.new.Promise):
+        (window.UIHelper.getUISelectionRects):
+
+        Helpers to fetch selection and caret rect information in the UI process.
+
 2017-08-21  Ryosuke Niwa  <rniwa@webkit.org>
 
         Consolidate the code to normalize MIME type in DataTransfer
index cbfe1ab..cac0ccc 100644 (file)
@@ -1,10 +1,12 @@
+PASS initialCaretRect.left is 6
+PASS initialCaretRect.top is 21
+PASS initialCaretRect.width is 3
+PASS initialCaretRect.height is 15
+PASS finalCaretRect.left is 6
+PASS finalCaretRect.top is 21
+PASS finalCaretRect.width is 3
+PASS finalCaretRect.height is 15
 PASS successfullyParsed is true
 
 TEST COMPLETE
-The initial caret rect is: [6 21 ; 3 15]
-The caret rect after scrolling 1000px down is: [6 21 ; 3 15]
-PASS finalCaretRect.top is initialCaretRect.top
-PASS finalCaretRect.left is initialCaretRect.left
-PASS finalCaretRect.width is initialCaretRect.width
-PASS finalCaretRect.height is initialCaretRect.height
 
index 4f927a1..a0ec9d0 100644 (file)
@@ -2,6 +2,7 @@
 <html>
 <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
 <head>
+    <script src="../../../resources/ui-helper.js"></script>
     <script src="../../../resources/js-test.js"></script>
     <style>
         body {
 
         div {
             background-image: linear-gradient(0deg, blue, red);
-            height: 4000px;
+            height: 10000px;
         }
     </style>
-    <script>
-    jsTestIsAsync = true;
-
-    function tapInInputScript(tapX, tapY)
-    {
-        return `(function() {
-            uiController.didShowKeyboardCallback = function() {
-                uiController.doAfterNextStablePresentationUpdate(function() {
-                    uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionCaretRect));
-                });
-            };
-            uiController.singleTapAtPoint(${tapX}, ${tapY}, function() { });
-        })()`;
-    }
-
-    function simulateScrollingScript(distance)
-    {
-        return `(function() {
-            uiController.stableStateOverride = false;
-            uiController.immediateScrollToOffset(0, ${distance});
-            uiController.stableStateOverride = true;
-            uiController.doAfterNextStablePresentationUpdate(function() {
-                uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionCaretRect));
-            });
-        })()`;
-    }
-
-    function toString(rect)
-    {
-        return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
-    }
-
-    function run()
-    {
-        if (!window.testRunner || !testRunner.runUIScript) {
-            description("To manually test, tap this input field and scroll up. The text caret should not end up outside of the input.");
-            return;
-        }
-
-        testRunner.runUIScript(tapInInputScript(window.innerWidth / 2, 30), initialCaretRect => {
-            initialCaretRect = JSON.parse(initialCaretRect);
-            window.initialCaretRect = initialCaretRect;
-            debug(`The initial caret rect is: ${toString(initialCaretRect)}`);
-            testRunner.runUIScript(simulateScrollingScript(1000), finalCaretRect => {
-                finalCaretRect = JSON.parse(finalCaretRect);
-                window.finalCaretRect = finalCaretRect;
-                debug(`The caret rect after scrolling 1000px down is: ${toString(finalCaretRect)}`);
-                shouldBe("finalCaretRect.top", "initialCaretRect.top");
-                shouldBe("finalCaretRect.left", "initialCaretRect.left");
-                shouldBe("finalCaretRect.width", "initialCaretRect.width");
-                shouldBe("finalCaretRect.height", "initialCaretRect.height");
-                finishJSTest();
-            });
-        });
-    }
-    </script>
 </head>
-<body onload=run()>
-    <input></input>
+<body>
+<div>
+<input id="input"></input>
+</div>
 </body>
+<script>
+jsTestIsAsync = true;
+
+(() => {
+    if (!window.testRunner || !testRunner.runUIScript) {
+        description("To manually test, tap this input field and scroll up. The text caret should not end up outside of the input.");
+        return;
+    }
 
+    UIHelper.activateAndWaitForInputSessionAt(innerWidth / 2, 30)
+    .then(() => UIHelper.getUICaretRect())
+    .then((rect) => {
+        initialCaretRect = rect;
+        shouldBe("initialCaretRect.left", "6");
+        shouldBe("initialCaretRect.top", "21");
+        shouldBe("initialCaretRect.width", "3");
+        shouldBe("initialCaretRect.height", "15");
+        document.body.scrollTop += 5000;
+        return UIHelper.getUICaretRect();
+    })
+    .then((rect) => {
+        finalCaretRect = rect;
+        shouldBe("finalCaretRect.left", "6");
+        shouldBe("finalCaretRect.top", "21");
+        shouldBe("finalCaretRect.width", "3");
+        shouldBe("finalCaretRect.height", "15");
+        finishJSTest();
+    });
+})();
+</script>
 </html>
index f789825..d473645 100644 (file)
@@ -1,5 +1,12 @@
+PASS initialCaretRect.left is 6
+PASS initialCaretRect.top is 21
+PASS initialCaretRect.width is 3
+PASS initialCaretRect.height is 15
+PASS finalCaretRect.left is 6
+PASS finalCaretRect.top is 5021
+PASS finalCaretRect.width is 3
+PASS finalCaretRect.height is 15
 PASS successfullyParsed is true
 
 TEST COMPLETE
-PASS finalCaretRect.top - initialCaretRect.top is 500
 
index 42f0fa0..80ae21c 100644 (file)
@@ -2,6 +2,7 @@
 <html>
 <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
 <head>
+    <script src="../../../resources/ui-helper.js"></script>
     <script src="../../../resources/js-test.js"></script>
     <style>
         body {
 
         div {
             background-image: linear-gradient(blue, red);
-            height: 4000px;
+            height: 10000px;
         }
     </style>
-    <script>
-    jsTestIsAsync = true;
-
-    function getInputViewBoundsAfterTappingInInputScript(tapX, tapY)
-    {
-        return `(function() {
-            uiController.didShowKeyboardCallback = function() {
-                uiController.doAfterNextStablePresentationUpdate(function() {
-                    uiController.uiScriptComplete(JSON.stringify(uiController.inputViewBounds));
-                });
-            };
-            uiController.singleTapAtPoint(${tapX}, ${tapY}, function() { });
-        })()`;
-    }
-
-    function getCaretRectAfterScrollToOffsetScript(distance)
-    {
-        return `(function() {
-            uiController.stableStateOverride = false;
-            uiController.immediateScrollToOffset(0, ${distance});
-            uiController.stableStateOverride = true;
-            uiController.doAfterNextStablePresentationUpdate(function() {
-                uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionCaretRect));
-            });
-        })()`;
-    }
-
-    function toString(rect)
-    {
-        return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
-    }
-
-    function run()
-    {
-        if (!window.testRunner || !testRunner.runUIScript) {
-            description("To manually test, tap this input field and scroll up. The text caret should not end up outside of the input.");
-            return;
-        }
-
-        testRunner.runUIScript(getInputViewBoundsAfterTappingInInputScript(window.innerWidth / 2, 30), (inputViewBounds) => {
-            window.inputViewBounds = inputViewBounds = JSON.parse(inputViewBounds);
-            testRunner.runUIScript(getCaretRectAfterScrollToOffsetScript(inputViewBounds.height), initialCaretRect => {
-                window.initialCaretRect = initialCaretRect = JSON.parse(initialCaretRect);
-                testRunner.runUIScript(getCaretRectAfterScrollToOffsetScript(inputViewBounds.height + 500), finalCaretRect => {
-                    window.finalCaretRect = finalCaretRect = JSON.parse(finalCaretRect);
-                    shouldBe("finalCaretRect.top - initialCaretRect.top", "500");
-                    finishJSTest();
-                });
-            });
-        });
-    }
-    </script>
 </head>
-<body onload=run()>
-    <input id="input"></input>
+<body>
+<div>
+<input id="input"></input>
+</div>
 </body>
+<script>
+jsTestIsAsync = true;
+
+(() => {
+    if (!window.testRunner || !testRunner.runUIScript) {
+        description("To manually test, tap this input field and scroll up. The text caret should not end up outside of the input.");
+        return;
+    }
 
+    UIHelper.activateAndWaitForInputSessionAt(innerWidth / 2, 30)
+    .then(() => UIHelper.getUICaretRect())
+    .then((rect) => {
+        initialCaretRect = rect;
+        shouldBe("initialCaretRect.left", "6");
+        shouldBe("initialCaretRect.top", "21");
+        shouldBe("initialCaretRect.width", "3");
+        shouldBe("initialCaretRect.height", "15");
+        document.body.scrollTop += 5000;
+        return UIHelper.getUICaretRect();
+    })
+    .then((rect) => {
+        finalCaretRect = rect;
+        shouldBe("finalCaretRect.left", "6");
+        shouldBe("finalCaretRect.top", "5021");
+        shouldBe("finalCaretRect.width", "3");
+        shouldBe("finalCaretRect.height", "15");
+        finishJSTest();
+    });
+})();
+</script>
 </html>
index 13d0591..d0c7692 100644 (file)
@@ -2,27 +2,40 @@
 <html>
 <head>
 <meta charset="utf-8">
+<script src="../../resources/ui-helper.js"></script>
 <script src="../../resources/js-test-pre.js"></script>
 </head>
 <body>
 <input type=password>
 <script>
 
+jsTestIsAsync = true;
+
 description("Verify that changing a password input's type updates secure input state.");
 
 var passwordInput = document.getElementsByTagName("input")[0];
 
 debug("A password input is focused:");
 passwordInput.focus();
-shouldBe("testRunner.secureEventInputIsEnabled", "true");
 
-debug("\nAfter changing the type to text:");
-passwordInput.type = "text";
-shouldBe("testRunner.secureEventInputIsEnabled", "false");
+UIHelper.ensurePresentationUpdate().then(() => {
+    shouldBe("testRunner.secureEventInputIsEnabled", "true");
+    debug("\nAfter changing the type to text:");
+    passwordInput.type = "text";
+})
+
+.then(() => UIHelper.ensurePresentationUpdate())
+.then(() => {
+    shouldBe("testRunner.secureEventInputIsEnabled", "false");
+    debug("\nAfter changing the type back to password:");
+    passwordInput.type = "password";
+})
 
-debug("\nAfter changing the type back to password:");
-passwordInput.type = "password";
-shouldBe("testRunner.secureEventInputIsEnabled", "true");
+.then(() => UIHelper.ensurePresentationUpdate())
+.then(() => {
+    shouldBe("testRunner.secureEventInputIsEnabled", "true");
+    finishJSTest();
+});
 
 </script>
 <script src="../../resources/js-test-post.js"></script>
index d8279c4..0523efa 100644 (file)
@@ -2,12 +2,14 @@
 <html>
 <head>
 <meta charset="utf-8">
+<script src="../../resources/ui-helper.js"></script>
 <script src="../../resources/js-test-pre.js"></script>
 </head>
 <body>
 <input type=password>
 <input type=text>
 <script>
+jsTestIsAsync = true;
 
 description("Verify that basic focusing/unfocusing updates secure input state.");
 
@@ -19,16 +21,25 @@ shouldBe("testRunner.secureEventInputIsEnabled", "false");
 
 debug("\nA password input is focused:");
 passwordInput.focus();
-shouldBe("testRunner.secureEventInputIsEnabled", "true");
 
-debug("\nA regular text input is focused:");
-textInput.focus();
-shouldBe("testRunner.secureEventInputIsEnabled", "false");
+UIHelper.ensurePresentationUpdate().then(() => {
+    shouldBe("testRunner.secureEventInputIsEnabled", "true");
+    debug("\nA regular text input is focused:");
+    textInput.focus();
+})
 
-debug("\nA password input is focused again:");
-passwordInput.focus();
-shouldBe("testRunner.secureEventInputIsEnabled", "true");
+.then(() => UIHelper.ensurePresentationUpdate())
+.then(() => {
+    shouldBe("testRunner.secureEventInputIsEnabled", "false");
+    debug("\nA password input is focused again:");
+    passwordInput.focus();
+})
 
+.then(() => UIHelper.ensurePresentationUpdate())
+.then(() => {
+    shouldBe("testRunner.secureEventInputIsEnabled", "true");
+    finishJSTest();
+});
 </script>
 <script src="../../resources/js-test-post.js"></script>
 </body>
index 2380844..333618b 100644 (file)
@@ -2,23 +2,36 @@
 <html>
 <head>
 <meta charset="utf-8">
+<script src="../../resources/ui-helper.js"></script>
 <script src="../../resources/js-test-pre.js"></script>
 </head>
 <body>
 <input type=password>
 <script>
 
+jsTestIsAsync = true;
+
 description("Verify that removing a password input from DOM tree disables secure input state.");
 
 var passwordInput = document.getElementsByTagName("input")[0];
 
 debug("A password input is focused:");
 passwordInput.focus();
-shouldBe("testRunner.secureEventInputIsEnabled", "true");
 
-debug("\nAfter deleting the input:");
-document.body.removeChild(passwordInput);
-shouldBe("testRunner.secureEventInputIsEnabled", "false");
+UIHelper.ensurePresentationUpdate().then(() => {
+    shouldBe("testRunner.secureEventInputIsEnabled", "true");
+    debug("\nAfter deleting the input:");
+    document.body.removeChild(passwordInput);
+})
+
+.then(() => {
+    return UIHelper.ensurePresentationUpdate()
+})
+
+.then(() => {
+    shouldBe("testRunner.secureEventInputIsEnabled", "false");
+    finishJSTest();
+})
 
 </script>
 <script src="../../resources/js-test-post.js"></script>
index e9b98ee..2dfa775 100644 (file)
@@ -1,5 +1,8 @@
 <!DOCTYPE html>
 <html>
+<head>
+<script src="../../resources/ui-helper.js"></script>
+</head>
 <body>
 <input type=password>
 <script>
@@ -10,10 +13,12 @@ if (window.testRunner)
 var passwordInput = document.getElementsByTagName("input")[0];
 
 passwordInput.focus();
-if (!testRunner.secureEventInputIsEnabled)
-    alert("FAIL: Secure event input is not enabled after focusing a password input");
+UIHelper.ensurePresentationUpdate().then(() => {
+    if (!testRunner.secureEventInputIsEnabled)
+        alert("FAIL: Secure event input is not enabled after focusing a password input");
 
-location = "resources/reset-state-on-navigation-target.html";
+    location = "resources/reset-state-on-navigation-target.html";
+})
 
 </script>
 </body>
index a0761db..b9c9df3 100644 (file)
@@ -1,11 +1,15 @@
+PASS initialSelectionRects.length is 1
+PASS initialSelectionRects[0].left is 0
+PASS initialSelectionRects[0].top is 0
+PASS initialSelectionRects[0].width is 309
+PASS initialSelectionRects[0].height is 114
+PASS finalSelectionRects.length is 1
+PASS finalSelectionRects[0].left is 0
+PASS finalSelectionRects[0].top is 0
+PASS finalSelectionRects[0].width is 309
+PASS finalSelectionRects[0].height is 114
 PASS successfullyParsed is true
 
 TEST COMPLETE
-After long pressing, the selection rects are: [0 0 ; 309 114]
-After scrolling 1000px down, the selection rects are: [0 0 ; 309 114]
-PASS finalSelectionRects[0].top is initialSelectionRects[0].top
-PASS finalSelectionRects[0].left is initialSelectionRects[0].left
-PASS finalSelectionRects[0].width is initialSelectionRects[0].width
-PASS finalSelectionRects[0].height is initialSelectionRects[0].height
 WebKit
 
index aab1a07..a7c2bba 100644 (file)
@@ -2,13 +2,14 @@
 <html>
 <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
 <head>
+    <script src="../../../resources/ui-helper.js"></script>
     <script src="../../../resources/js-test.js"></script>
     <style>
         body {
             margin: 0;
         }
 
-        #fixed {
+        #absolute {
             width: 100vw;
             height: 50px;
             position: absolute;
 
         #content {
             background-image: linear-gradient(blue, red);
-            height: 4000px;
+            height: 10000px;
         }
     </style>
     <script>
     jsTestIsAsync = true;
 
-    function toString(rect)
+    function selectTextAt(tapX, tapY)
     {
-        return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
-    }
-
-    function selectFixedTextScript(tapX, tapY)
-    {
-        return `
-        (function() {
-            uiController.longPressAtPoint(${tapX}, ${tapY}, function() {
-                uiController.doAfterNextStablePresentationUpdate(function() {
-                    uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
-                });
-            });
-        })();`;
-    }
-
-    function simulateScrollingScript(distance)
-    {
-        return `(function() {
-            uiController.stableStateOverride = false;
-            uiController.immediateScrollToOffset(0, ${distance});
-            uiController.stableStateOverride = true;
-            uiController.doAfterNextStablePresentationUpdate(function() {
-                uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
-            });
-        })()`;
-    }
-
-    function toString(rect)
-    {
-        return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
+        return new Promise(resolve => {
+            testRunner.runUIScript(`
+                (function() {
+                    uiController.longPressAtPoint(${tapX}, ${tapY}, function() {
+                        uiController.uiScriptComplete("Done");
+                    });
+                })();`, resolve);
+        });
     }
 
     function run()
     {
         if (!window.testRunner || !testRunner.runUIScript) {
-            description("To manually test, long press this text and scroll up. The selection rect should be in the expected place (at the very top of the page) after scrolling.");
+            description("To manually test, long press this text and scroll up. The selection rect should be in the expected place (in the absolutely positioned div) after scrolling.");
             return;
         }
 
-        testRunner.runUIScript(selectFixedTextScript(50, 50), initialSelectionRects => {
-            initialSelectionRects = JSON.parse(initialSelectionRects);
-            window.initialSelectionRects = initialSelectionRects;
-            debug(`After long pressing, the selection rects are: ${initialSelectionRects.map(toString)}`);
-            testRunner.runUIScript(simulateScrollingScript(1000), finalSelectionRects => {
-                finalSelectionRects = JSON.parse(finalSelectionRects);
-                window.finalSelectionRects = finalSelectionRects;
-                debug(`After scrolling 1000px down, the selection rects are: ${finalSelectionRects.map(toString)}`);
-                shouldBe("finalSelectionRects[0].top", "initialSelectionRects[0].top");
-                shouldBe("finalSelectionRects[0].left", "initialSelectionRects[0].left");
-                shouldBe("finalSelectionRects[0].width", "initialSelectionRects[0].width");
-                shouldBe("finalSelectionRects[0].height", "initialSelectionRects[0].height");
-                finishJSTest();
-            });
+        selectTextAt(50, 50)
+        .then(() => UIHelper.getUISelectionRects())
+        .then((rects) => {
+            initialSelectionRects = rects;
+            shouldBe("initialSelectionRects.length", "1");
+            shouldBe("initialSelectionRects[0].left", "0");
+            shouldBe("initialSelectionRects[0].top", "0");
+            shouldBe("initialSelectionRects[0].width", "309");
+            shouldBe("initialSelectionRects[0].height", "114");
+            document.body.scrollTop += 5000;
+            return UIHelper.getUISelectionRects();
+        })
+        .then((rects) => {
+            finalSelectionRects = rects;
+            shouldBe("finalSelectionRects.length", "1");
+            shouldBe("finalSelectionRects[0].left", "0");
+            shouldBe("finalSelectionRects[0].top", "0");
+            shouldBe("finalSelectionRects[0].width", "309");
+            shouldBe("finalSelectionRects[0].height", "114");
+            finishJSTest();
         });
     }
     </script>
 </head>
 <body onload=run()>
-    <div id="fixed">WebKit</div>
+    <div id="absolute">WebKit</div>
     <div id="content"></div>
 </body>
-</html>
\ No newline at end of file
+</html>
index 23e0231..9917b96 100644 (file)
@@ -1,10 +1,15 @@
+PASS initialSelectionRects.length is 1
+PASS initialSelectionRects[0].left is 0
+PASS initialSelectionRects[0].top is 0
+PASS initialSelectionRects[0].width is 309
+PASS initialSelectionRects[0].height is 114
+PASS finalSelectionRects.length is 1
+PASS finalSelectionRects[0].left is 0
+PASS finalSelectionRects[0].top is 5000
+PASS finalSelectionRects[0].width is 309
+PASS finalSelectionRects[0].height is 114
 PASS successfullyParsed is true
 
 TEST COMPLETE
-After long pressing, the selection rects are: [0 0 ; 309 114]
-After scrolling 1000px down, the selection rects are: [0 1000 ; 309 114]
-PASS initialSelectionRects.length is 1
-PASS finalSelectionRects.length is 1
-PASS finalSelectionRects[0].top - initialSelectionRects[0].top is 1000
 WebKit
 
index 248be1d..8da8b39 100644 (file)
@@ -2,6 +2,7 @@
 <html>
 <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
 <head>
+    <script src="../../../resources/ui-helper.js"></script>
     <script src="../../../resources/js-test.js"></script>
     <style>
         body {
 
         #content {
             background-image: linear-gradient(blue, red);
-            height: 4000px;
+            height: 10000px;
         }
     </style>
     <script>
     jsTestIsAsync = true;
 
-    function toString(rect)
+    function selectTextAt(tapX, tapY)
     {
-        return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
-    }
-
-    function selectFixedTextScript(tapX, tapY)
-    {
-        return `
-        (function() {
-            uiController.longPressAtPoint(${tapX}, ${tapY}, function() {
-                uiController.doAfterNextStablePresentationUpdate(function() {
-                    uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
-                });
-            });
-        })();`;
-    }
-
-    function simulateScrollingScript(distance)
-    {
-        return `(function() {
-            uiController.stableStateOverride = false;
-            uiController.immediateScrollToOffset(0, ${distance});
-            uiController.stableStateOverride = true;
-            uiController.doAfterNextStablePresentationUpdate(function() {
-                uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
-            });
-        })()`;
-    }
-
-    function toString(rect)
-    {
-        return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
+        return new Promise(resolve => {
+            testRunner.runUIScript(`
+                (function() {
+                    uiController.longPressAtPoint(${tapX}, ${tapY}, function() {
+                        uiController.uiScriptComplete("Done");
+                    });
+                })();`, resolve);
+        });
     }
 
     function run()
             return;
         }
 
-        testRunner.runUIScript(selectFixedTextScript(50, 50), initialSelectionRects => {
-            initialSelectionRects = JSON.parse(initialSelectionRects);
-            window.initialSelectionRects = initialSelectionRects;
-            debug(`After long pressing, the selection rects are: ${initialSelectionRects.map(toString)}`);
-            testRunner.runUIScript(simulateScrollingScript(1000), finalSelectionRects => {
-                finalSelectionRects = JSON.parse(finalSelectionRects);
-                window.finalSelectionRects = finalSelectionRects;
-                debug(`After scrolling 1000px down, the selection rects are: ${finalSelectionRects.map(toString)}`);
-                shouldBe("initialSelectionRects.length", "1");
-                shouldBe("finalSelectionRects.length", "1");
-                shouldBe("finalSelectionRects[0].top - initialSelectionRects[0].top", "1000");
-                finishJSTest();
-            });
+        selectTextAt(50, 50)
+        .then(() => UIHelper.getUISelectionRects())
+        .then((rects) => {
+            initialSelectionRects = rects;
+            shouldBe("initialSelectionRects.length", "1");
+            shouldBe("initialSelectionRects[0].left", "0");
+            shouldBe("initialSelectionRects[0].top", "0");
+            shouldBe("initialSelectionRects[0].width", "309");
+            shouldBe("initialSelectionRects[0].height", "114");
+            document.body.scrollTop += 5000;
+            return UIHelper.getUISelectionRects();
+        })
+        .then((rects) => {
+            finalSelectionRects = rects;
+            shouldBe("finalSelectionRects.length", "1");
+            shouldBe("finalSelectionRects[0].left", "0");
+            shouldBe("finalSelectionRects[0].top", "5000");
+            shouldBe("finalSelectionRects[0].width", "309");
+            shouldBe("finalSelectionRects[0].height", "114");
+            finishJSTest();
         });
     }
     </script>
@@ -87,4 +73,4 @@
     <div id="fixed">WebKit</div>
     <div id="content"></div>
 </body>
-</html>
\ No newline at end of file
+</html>
index 1e41ca5..491323d 100644 (file)
@@ -64,7 +64,6 @@ layer at (0,0) size 800x600
       RenderBlock {P} at (0,238) size 784x58 [border: (2px solid #0000FF)]
         RenderText {#text} at (14,15) size 36x28
           text run at (14,15) width 36: "xxx"
-      RenderBlock (anonymous) at (0,320) size 784x0
       RenderBlock {P} at (0,320) size 784x58 [border: (2px solid #0000FF)]
         RenderBR {BR} at (14,15) size 0x28 [bgcolor=#008000]
       RenderBlock {P} at (0,402) size 784x58 [border: (2px solid #0000FF)]
index a61cffe..d00e802 100644 (file)
@@ -54,5 +54,4 @@ layer at (0,0) size 800x600
               text run at (2,3) width 20: "fo"
           RenderText {#text} at (21,3) size 13x28
             text run at (21,3) width 13: "x"
-        RenderBlock (anonymous) at (0,34) size 784x0
 caret: position 3 of child 0 {#text} of child 0 {B} of child 1 {DIV} of child 3 {DIV} of body
index 950ad32..e963cd3 100644 (file)
@@ -15,5 +15,4 @@ layer at (0,0) size 800x600
           RenderInline {FONT} at (0,0) size 159x19 [color=#0000FF]
             RenderText {#text} at (150,0) size 159x19
               text run at (150,0) width 159: "This text should be blue."
-      RenderBlock (anonymous) at (0,76) size 784x0
 caret: position 25 of child 0 {#text} of child 1 {FONT} of child 0 {SPAN} of child 2 {DIV} of body
index 9dec428..bf35134 100644 (file)
@@ -678,8 +678,6 @@ fast/events/context-activated-by-key-event.html [ Skip ]
 
 webkit.org/b/168933 fast/regions/inline-block-inside-anonymous-overflow-with-covered-controls.html [ Pass ImageOnlyFailure ]
 
-webkit.org/b/168346 [ Sierra Debug ] editing/selection/move-by-word-visually-multi-space.html [ Pass Timeout ]
-
 webkit.org/b/169621 imported/w3c/web-platform-tests/IndexedDB/fire-error-event-exception.html [ Pass Failure ]
 webkit.org/b/169760 imported/w3c/web-platform-tests/IndexedDB/fire-success-event-exception.html [ Pass Failure ]
 
index 768b8e9..a3c8f76 100644 (file)
@@ -86,5 +86,6 @@ layer at (0,0) size 800x600
           RenderText {#text} at (164,14) size 78x28
             text run at (164,14) width 78: " xxxxxx"
         RenderInline {SPAN} at (0,0) size 0x28
+      RenderBlock (anonymous) at (0,56) size 784x0
 selection start: position 0 of child 1 {#text} of child 1 {DIV} of body
 selection end:   position 6 of child 1 {#text} of child 1 {DIV} of body
index 068f701..788fdf8 100644 (file)
@@ -47,6 +47,69 @@ window.UIHelper = class UIHelper {
         });
     }
 
+    static ensurePresentationUpdate()
+    {
+        if (!this.isWebKit2()) {
+            testRunner.display();
+            return Promise.resolve();
+        }
+
+        return new Promise(resolve => {
+            testRunner.runUIScript(`
+                uiController.doAfterPresentationUpdate(function() {
+                    uiController.uiScriptComplete('Done');
+                });`, resolve);
+        });
+    }
+
+    static activateAndWaitForInputSessionAt(x, y)
+    {
+        if (!this.isWebKit2() || !this.isIOS())
+            return this.activateAt(x, y);
+
+        return new Promise(resolve => {
+            testRunner.runUIScript(`
+                (function() {
+                    uiController.didShowKeyboardCallback = function() {
+                        uiController.uiScriptComplete("Done");
+                    };
+                    uiController.singleTapAtPoint(${x}, ${y}, function() { });
+                })()`, resolve);
+        });
+    }
+
+    static getUICaretRect()
+    {
+        if (!this.isWebKit2() || !this.isIOS())
+            return Promise.resolve();
+
+        return new Promise(resolve => {
+            testRunner.runUIScript(`(function() {
+                uiController.doAfterNextStablePresentationUpdate(function() {
+                    uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionCaretRect));
+                });
+            })()`, jsonString => {
+                resolve(JSON.parse(jsonString));
+            });
+        });
+    }
+
+    static getUISelectionRects()
+    {
+        if (!this.isWebKit2() || !this.isIOS())
+            return Promise.resolve();
+
+        return new Promise(resolve => {
+            testRunner.runUIScript(`(function() {
+                uiController.doAfterNextStablePresentationUpdate(function() {
+                    uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
+                });
+            })()`, jsonString => {
+                resolve(JSON.parse(jsonString));
+            });
+        });
+    }
+
     static wait(promise)
     {
         testRunner.waitUntilDone();
index 8d18808..56fb54d 100644 (file)
@@ -1,3 +1,41 @@
+2017-08-22  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+        https://bugs.webkit.org/show_bug.cgi?id=175370
+        <rdar://problem/33799806>
+
+        Reviewed by Ryosuke Niwa.
+
+        Remove didChangeSelectionAndUpdateLayout -- EditorState updates that are scheduled due to missing post-layout
+        data will now be scheduled for the next presentation update. Additionally, add editor client hooks to notify the
+        WebKit layer when we've updated the current composition. See WebKit ChangeLog for more details. This patch
+        adjusts and rebaselines existing layout tests.
+
+        * editing/Editor.cpp:
+        (WebCore::SetCompositionScope::SetCompositionScope):
+        (WebCore::SetCompositionScope::~SetCompositionScope):
+
+        Introduce a helper RAII class to ensure that we ignore selection changes during the scope of
+        Editor::setComposition and call out to the client with WebEditorClient::didUpdateComposition afterwards. This
+        also maintains a UserTypingGestureIndicator over its lifetime, so we don't additionally need to create a
+        UserTypingGestureIndicator in Editor::setComposition.
+
+        (WebCore::Editor::setComposition):
+        * editing/FrameSelection.cpp:
+        (WebCore::FrameSelection::setSelection):
+        (WebCore::FrameSelection::updateAndRevealSelection):
+        (WebCore::FrameSelection::setSelectedRange):
+        * editing/FrameSelection.h:
+        (WebCore::FrameSelection::defaultSetSelectionOptions):
+
+        Plumb state about whether or not the selection change was triggered by the user to FrameSelection::setSelection,
+        and if so, notify the editing client. A separate SetSelectionOptions flag is used here instead of
+        RevealSelection to avoid calling out to the client in places where we want to reveal the selection regardless of
+        whether or not the selection is user triggered.
+
+        * loader/EmptyClients.cpp:
+        * page/EditorClient.h:
+
 2017-08-21  Ryosuke Niwa  <rniwa@webkit.org>
 
         Consolidate the code to normalize MIME type in DataTransfer
index 47a7405..1854505 100644 (file)
@@ -1749,12 +1749,30 @@ void Editor::confirmComposition(const String& text)
     setComposition(text, ConfirmComposition);
 }
 
+class SetCompositionScope {
+public:
+    SetCompositionScope(Frame& frame)
+        : m_frame(frame)
+        , m_typingGestureIndicator(frame)
+    {
+        m_frame->editor().setIgnoreSelectionChanges(true);
+    }
+
+    ~SetCompositionScope()
+    {
+        m_frame->editor().setIgnoreSelectionChanges(false);
+        if (auto* editorClient = m_frame->editor().client())
+            editorClient->didUpdateComposition();
+    }
+
+    Ref<Frame> m_frame;
+    UserTypingGestureIndicator m_typingGestureIndicator;
+};
+
 void Editor::setComposition(const String& text, SetCompositionMode mode)
 {
     ASSERT(mode == ConfirmComposition || mode == CancelComposition);
-    UserTypingGestureIndicator typingGestureIndicator(m_frame);
-
-    setIgnoreSelectionChanges(true);
+    SetCompositionScope setCompositionScope(m_frame);
 
     if (mode == CancelComposition)
         ASSERT(text == emptyString());
@@ -1764,10 +1782,8 @@ void Editor::setComposition(const String& text, SetCompositionMode mode)
     m_compositionNode = nullptr;
     m_customCompositionUnderlines.clear();
 
-    if (m_frame.selection().isNone()) {
-        setIgnoreSelectionChanges(false);
+    if (m_frame.selection().isNone())
         return;
-    }
 
     // Always delete the current composition before inserting the finalized composition text if we're confirming our composition.
     // Our default behavior (if the beforeinput event is not prevented) is to insert the finalized composition text back in.
@@ -1784,17 +1800,11 @@ void Editor::setComposition(const String& text, SetCompositionMode mode)
         // An open typing command that disagrees about current selection would cause issues with typing later on.
         TypingCommand::closeTyping(&m_frame);
     }
-
-    setIgnoreSelectionChanges(false);
 }
 
 void Editor::setComposition(const String& text, const Vector<CompositionUnderline>& underlines, unsigned selectionStart, unsigned selectionEnd)
 {
-    Ref<Frame> protection(m_frame);
-
-    UserTypingGestureIndicator typingGestureIndicator(m_frame);
-
-    setIgnoreSelectionChanges(true);
+    SetCompositionScope setCompositionScope(m_frame);
 
     // Updates styles before setting selection for composition to prevent
     // inserting the previous composition text into text nodes oddly.
@@ -1803,10 +1813,8 @@ void Editor::setComposition(const String& text, const Vector<CompositionUnderlin
 
     selectComposition();
 
-    if (m_frame.selection().isNone()) {
-        setIgnoreSelectionChanges(false);
+    if (m_frame.selection().isNone())
         return;
-    }
 
     String originalText = selectedText();
     bool isStartingToRecomposeExistingRange = !text.isEmpty() && selectionStart < selectionEnd && !hasComposition();
@@ -1897,8 +1905,6 @@ void Editor::setComposition(const String& text, const Vector<CompositionUnderlin
         }
     }
 
-    setIgnoreSelectionChanges(false);
-
 #if PLATFORM(IOS)        
     client()->stopDelayingAndCoalescingContentChangeNotifications();
 #endif
index 071eb44..a857ede 100644 (file)
@@ -367,6 +367,11 @@ void FrameSelection::setSelection(const VisibleSelection& selection, SetSelectio
         return;
 
     updateAndRevealSelection(intent);
+
+    if (options & IsUserTriggered) {
+        if (auto* client = m_frame->editor().client())
+            client->didEndUserTriggeredSelectionChanges();
+    }
 }
 
 static void updateSelectionByUpdatingLayoutOrStyle(Frame& frame)
@@ -406,9 +411,6 @@ void FrameSelection::updateAndRevealSelection(const AXTextStateChangeIntent& int
     }
 
     notifyAccessibilityForSelectionChange(intent);
-
-    if (auto* client = m_frame->editor().client())
-        client->didChangeSelectionAndUpdateLayout();
 }
 
 void FrameSelection::updateDataDetectorsForSelection()
@@ -1972,7 +1974,7 @@ bool FrameSelection::setSelectedRange(Range* range, EAffinity affinity, bool clo
             return false;
     }
 
-    setSelection(newSelection, ClearTypingStyle | (closeTyping ? CloseTyping : 0));
+    setSelection(newSelection, ClearTypingStyle | (closeTyping ? CloseTyping : 0) | (userTriggered == UserTriggered ? IsUserTriggered : 0));
     return true;
 }
 
index d35ef7c..c6ae93d 100644 (file)
@@ -126,11 +126,12 @@ public:
         DoNotSetFocus = 1 << 4,
         DictationTriggered = 1 << 5,
         RevealSelection = 1 << 6,
+        IsUserTriggered = 1 << 7,
     };
     typedef unsigned SetSelectionOptions; // Union of values in SetSelectionOption and EUserTriggered
     static inline SetSelectionOptions defaultSetSelectionOptions(EUserTriggered userTriggered = NotUserTriggered)
     {
-        return CloseTyping | ClearTypingStyle | (userTriggered ? (RevealSelection | FireSelectEvent) : 0);
+        return CloseTyping | ClearTypingStyle | (userTriggered ? (RevealSelection | FireSelectEvent | IsUserTriggered) : 0);
     }
 
     WEBCORE_EXPORT explicit FrameSelection(Frame* = nullptr);
index 7539e6b..781c4b0 100644 (file)
@@ -166,11 +166,12 @@ private:
     void didBeginEditing() final { }
     void respondToChangedContents() final { }
     void respondToChangedSelection(Frame*) final { }
-    void didChangeSelectionAndUpdateLayout() final { }
     void updateEditorStateAfterLayoutIfEditabilityChanged() final { }
     void discardedComposition(Frame*) final { }
     void canceledComposition() final { }
+    void didUpdateComposition() final { }
     void didEndEditing() final { }
+    void didEndUserTriggeredSelectionChanges() final { }
     void willWriteSelectionToPasteboard(Range*) final { }
     void didWriteSelectionToPasteboard() final { }
     void getClientPasteboardDataForRange(Range*, Vector<String>&, Vector<RefPtr<SharedBuffer>>&) final { }
index 0695877..e135ed6 100644 (file)
@@ -90,7 +90,7 @@ public:
     virtual void didBeginEditing() = 0;
     virtual void respondToChangedContents() = 0;
     virtual void respondToChangedSelection(Frame*) = 0;
-    virtual void didChangeSelectionAndUpdateLayout() = 0;
+    virtual void didEndUserTriggeredSelectionChanges() = 0;
     virtual void updateEditorStateAfterLayoutIfEditabilityChanged() = 0;
     virtual void didEndEditing() = 0;
     virtual void willWriteSelectionToPasteboard(Range*) = 0;
@@ -103,6 +103,7 @@ public:
     // This function is not called when a composition is closed per a request from an input method.
     virtual void discardedComposition(Frame*) = 0;
     virtual void canceledComposition() = 0;
+    virtual void didUpdateComposition() = 0;
 
     virtual void registerUndoStep(UndoStep&) = 0;
     virtual void registerRedoStep(UndoStep&) = 0;
index 10c7d75..f9bb0d0 100644 (file)
@@ -1,3 +1,130 @@
+2017-08-22  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+        https://bugs.webkit.org/show_bug.cgi?id=175370
+        <rdar://problem/33799806>
+
+        Reviewed by Ryosuke Niwa.
+
+        See per-method comments for more detail. WebPage::didChangeSelection now schedules EditorState updates to be sent
+        during the next layer tree transaction rather than sending them synchronously. To ensure that iOS and Mac continue
+        to behave correctly w.r.t. EditorState updates, we immediately dispatch EditorStates in the following cases:
+        - After the composition changes, is confirmed, or is canceled.
+        - After an edit command is executed.
+        - After ending user-triggered selection changes.
+
+        * Shared/mac/RemoteLayerTreeTransaction.h:
+        (WebKit::RemoteLayerTreeTransaction::hasEditorState const):
+        (WebKit::RemoteLayerTreeTransaction::editorState const):
+        (WebKit::RemoteLayerTreeTransaction::setEditorState):
+
+        Attaches an optional EditorState to the RemoteLayerTreeTransaction. This EditorState is computed and sent over
+        when setting up the transaction in WebPage, if something previously scheduled an EditorState update.
+
+        * Shared/mac/RemoteLayerTreeTransaction.mm:
+        (WebKit::RemoteLayerTreeTransaction::encode const):
+        (WebKit::RemoteLayerTreeTransaction::decode):
+
+        Add coder support for sending over a layer tree transaction's EditorState.
+
+        * UIProcess/API/Cocoa/WKViewPrivate.h:
+        * UIProcess/API/mac/WKView.mm:
+        (-[WKView _doAfterNextPresentationUpdate:]):
+
+        Add _doAfterNextPresentationUpdate to WKView (used in TestWebKitAPI -- refer to
+        WebKitAgnosticTest::waitForNextPresentationUpdate).
+
+        * UIProcess/DrawingAreaProxy.h:
+        (WebKit::DrawingAreaProxy::dispatchPresentationCallbacksAfterFlushingLayers):
+        * UIProcess/DrawingAreaProxy.messages.in:
+
+        Add a new IPC messages, DispatchPresentationCallbacksAfterFlushingLayers, to invoke in-flight presentation
+        callbacks in the UI process following a layer flush in the web process.
+
+        * UIProcess/WebPageProxy.h:
+        * UIProcess/mac/RemoteLayerTreeDrawingAreaProxy.mm:
+        (WebKit::RemoteLayerTreeDrawingAreaProxy::commitLayerTree):
+        * UIProcess/mac/TiledCoreAnimationDrawingAreaProxy.h:
+        * UIProcess/mac/TiledCoreAnimationDrawingAreaProxy.mm:
+        (WebKit::TiledCoreAnimationDrawingAreaProxy::~TiledCoreAnimationDrawingAreaProxy):
+        (WebKit::TiledCoreAnimationDrawingAreaProxy::dispatchAfterEnsuringDrawing):
+        (WebKit::TiledCoreAnimationDrawingAreaProxy::dispatchPresentationCallbacksAfterFlushingLayers):
+
+        Run all pending _doAfterNextPresentationUpdate callbacks.
+
+        * WebProcess/WebCoreSupport/WebEditorClient.cpp:
+        (WebKit::WebEditorClient::didApplyStyle):
+        (WebKit::WebEditorClient::respondToChangedContents):
+        (WebKit::WebEditorClient::didEndUserTriggeredSelectionChanges):
+        (WebKit::WebEditorClient::didUpdateComposition):
+
+        Forward editor client calls to the WebPage.
+
+        (WebKit::WebEditorClient::didChangeSelectionAndUpdateLayout): Deleted.
+        * WebProcess/WebCoreSupport/WebEditorClient.h:
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::WebPage::editorState const):
+        (WebKit::WebPage::updateEditorStateAfterLayoutIfEditabilityChanged):
+        (WebKit::WebPage::willCommitLayerTree):
+        (WebKit::WebPage::didApplyStyle):
+
+        Allow style application to immediately trigger EditorState updates, if we're not currently ignoring selection
+        changes in the Editor.
+
+        (WebKit::WebPage::didChangeContents):
+
+        Allow applying top-level edit commands to immediately trigger EditorState updates, if we're not currently
+        ignoring selection changes in the Editor.
+
+        (WebKit::WebPage::didChangeSelection):
+        (WebKit::WebPage::didUpdateComposition):
+        (WebKit::WebPage::didEndUserTriggeredSelectionChanges):
+        (WebKit::WebPage::discardedComposition):
+        (WebKit::WebPage::canceledComposition):
+
+        When handling composition updates, always send an EditorState to the UI process. Unlike other cases, IME
+        requires immediate EditorState data, so we need to be explicit here in sending updates right away.
+
+        (WebKit::WebPage::sendEditorStateUpdate):
+        (WebKit::WebPage::sendPartialEditorStateAndSchedulePostLayoutUpdate):
+        (WebKit::WebPage::flushPendingEditorStateUpdate):
+
+        Helper methods to schedule an EditorState update to be sent upon the next layer tree update, or flush any
+        pending EditorState update that has been scheduled. The private, more aggressive variant of this is
+        sendEditorStateUpdate, which ignores whether or not there was already an EditorState update scheduled, and sends
+        one anyways (this still fulfills any EditorState update that was previously scheduled).
+
+        These helper methods are treated as no-ops when invoked while ignoring selection changes. This is to prevent
+        temporary selection state and editor commands during operations such as text indicator snapshotting from pushing
+        bogus information about transient editor states to the UI process.
+
+        (WebKit::WebPage::sendPostLayoutEditorStateIfNeeded): Deleted.
+        * WebProcess/WebPage/WebPage.h:
+        * WebProcess/WebPage/ios/WebPageIOS.mm:
+        (WebKit::WebPage::platformEditorState const):
+        (WebKit::WebPage::executeEditCommandWithCallback):
+        (WebKit::selectionIsInsideFixedPositionContainer):
+        (WebKit::WebPage::updateVisibleContentRects):
+
+        Fix a hack that was computing an EditorState to figure out whether the current selection starts or ends in a
+        fixed position container. Factors out relevant logic into a separate helper, and also schedules an EditorState
+        update instead of immediately computing it.
+
+        * WebProcess/WebPage/mac/TiledCoreAnimationDrawingArea.h:
+        * WebProcess/WebPage/mac/TiledCoreAnimationDrawingArea.mm:
+        (WebKit::TiledCoreAnimationDrawingArea::addTransactionCallbackID):
+
+        Add support for registering and dispatching presentation callbacks that hook into the layer flush lifecycle,
+        using the tiled CA drawing area. These are used by Mac LayoutTests and API tests that need to wait until the
+        next flush before checking for state that depends on EditorState updates in the UI process.
+
+        (WebKit::TiledCoreAnimationDrawingArea::flushLayers):
+
+        Tell the WebPage to flush any pending EditorState updates.
+
+        * WebProcess/WebPage/mac/WebPageMac.mm:
+        (WebKit::WebPage::platformEditorState const):
+
 2017-08-22  Brent Fulgham  <bfulgham@apple.com>
 
         Relax keychain access to permit users to permanently allow client certificates
index 2a45fa2..9bf2f08 100644 (file)
@@ -26,6 +26,7 @@
 #ifndef RemoteLayerTreeTransaction_h
 #define RemoteLayerTreeTransaction_h
 
+#include "EditorState.h"
 #include "GenericCallback.h"
 #include "PlatformCAAnimationRemote.h"
 #include "RemoteLayerBackingStore.h"
@@ -268,6 +269,10 @@ public:
 
     WebCore::LayoutMilestones newlyReachedLayoutMilestones() const { return m_newlyReachedLayoutMilestones; }
     void setNewlyReachedLayoutMilestones(WebCore::LayoutMilestones milestones) { m_newlyReachedLayoutMilestones = milestones; }
+
+    bool hasEditorState() const { return !!m_editorState; }
+    const EditorState& editorState() const { return m_editorState.value(); }
+    void setEditorState(const EditorState& editorState) { m_editorState = editorState; }
     
 private:
     WebCore::GraphicsLayer::PlatformLayerID m_rootLayerID;
@@ -304,6 +309,8 @@ private:
     bool m_viewportMetaTagWidthWasExplicit { false };
     bool m_viewportMetaTagCameFromImageDocument { false };
     bool m_isInStableState { false };
+
+    std::optional<EditorState> m_editorState { std::nullopt };
 };
 
 } // namespace WebKit
index 84f8c5e..e94f4e9 100644 (file)
@@ -558,6 +558,10 @@ void RemoteLayerTreeTransaction::encode(IPC::Encoder& encoder) const
     encoder << m_isInStableState;
 
     encoder << m_callbackIDs;
+
+    encoder << hasEditorState();
+    if (m_editorState)
+        encoder << *m_editorState;
 }
 
 bool RemoteLayerTreeTransaction::decode(IPC::Decoder& decoder, RemoteLayerTreeTransaction& result)
@@ -673,6 +677,17 @@ bool RemoteLayerTreeTransaction::decode(IPC::Decoder& decoder, RemoteLayerTreeTr
     if (!decoder.decode(result.m_callbackIDs))
         return false;
 
+    bool hasEditorState;
+    if (!decoder.decode(hasEditorState))
+        return false;
+
+    if (hasEditorState) {
+        EditorState editorState;
+        if (!decoder.decode(editorState))
+            return false;
+        result.setEditorState(editorState);
+    }
+
     return true;
 }
 
index bdd9f6a..cb6ee1d 100644 (file)
 - (void)_addMediaPlaybackControlsView:(id)mediaPlaybackControlsView;
 - (void)_removeMediaPlaybackControlsView;
 
+- (void)_doAfterNextPresentationUpdate:(void (^)(void))updateBlock WK_API_AVAILABLE(macosx(WK_MAC_TBA));
+
 @end
 
 #endif // !TARGET_OS_IPHONE
index 3037479..9d1268b 100644 (file)
@@ -1592,6 +1592,15 @@ static _WKOverlayScrollbarStyle toAPIScrollbarStyle(std::optional<WebCore::Scrol
 {
 }
 
+- (void)_doAfterNextPresentationUpdate:(void (^)(void))updateBlock
+{
+    auto updateBlockCopy = makeBlockPtr(updateBlock);
+    _data->_impl->page().callAfterNextPresentationUpdate([updateBlockCopy](WebKit::CallbackBase::Error error) {
+        if (updateBlockCopy)
+            updateBlockCopy();
+    });
+}
+
 @end
 
 #endif // PLATFORM(MAC)
index e96cf73..50a297c 100644 (file)
@@ -106,6 +106,8 @@ public:
     virtual WebCore::MachSendRight createFence();
 #endif
 
+    virtual void dispatchPresentationCallbacksAfterFlushingLayers(const Vector<CallbackID>&) { }
+
 protected:
     explicit DrawingAreaProxy(DrawingAreaType, WebPageProxy&);
 
index 47360e3..3e58de5 100644 (file)
@@ -26,6 +26,7 @@ messages -> DrawingAreaProxy {
     EnterAcceleratedCompositingMode(uint64_t backingStoreStateID, WebKit::LayerTreeContext context)
     ExitAcceleratedCompositingMode(uint64_t backingStoreStateID, WebKit::UpdateInfo updateInfo)
     UpdateAcceleratedCompositingMode(uint64_t backingStoreStateID, WebKit::LayerTreeContext context)
+    DispatchPresentationCallbacksAfterFlushingLayers(Vector<WebKit::CallbackID> callbackIDs);
 
 #if PLATFORM(COCOA)
     // Used by TiledCoreAnimationDrawingAreaProxy.
index 09f4968..9fa015f 100644 (file)
@@ -1200,6 +1200,7 @@ public:
 #if PLATFORM(COCOA)
     void createSandboxExtensionsIfNeeded(const Vector<String>& files, SandboxExtension::Handle& fileReadHandle, SandboxExtension::HandleArray& fileUploadHandles);
 #endif
+    void editorStateChanged(const EditorState&);
 
 private:
     WebPageProxy(PageClient&, WebProcessProxy&, uint64_t pageID, Ref<API::PageConfiguration>&&);
@@ -1363,7 +1364,6 @@ private:
     void didEndColorPicker() override;
 #endif
 
-    void editorStateChanged(const EditorState&);
     void compositionWasCanceled();
     void setHasHadSelectionChangesFromUserInteraction(bool);
     void setNeedsHiddenContentEditableQuirk(bool);
index fcc435e..54b17d1 100644 (file)
@@ -185,6 +185,9 @@ void RemoteLayerTreeDrawingAreaProxy::commitLayerTree(const RemoteLayerTreeTrans
     ASSERT(layerTreeTransaction.transactionID() == m_lastVisibleTransactionID + 1);
     m_transactionIDForPendingCACommit = layerTreeTransaction.transactionID();
 
+    if (layerTreeTransaction.hasEditorState())
+        m_webPageProxy.editorStateChanged(layerTreeTransaction.editorState());
+
     if (m_remoteLayerTreeHost.updateLayerTree(layerTreeTransaction)) {
         if (layerTreeTransaction.transactionID() >= m_transactionIDForUnhidingContent)
             m_webPageProxy.setAcceleratedCompositingRootLayer(m_remoteLayerTreeHost.rootLayer());
index 4ccf564..f6eb62e 100644 (file)
@@ -52,6 +52,7 @@ private:
 
     void waitForDidUpdateActivityState() override;
     void dispatchAfterEnsuringDrawing(WTF::Function<void (CallbackBase::Error)>&&) override;
+    void dispatchPresentationCallbacksAfterFlushingLayers(const Vector<CallbackID>&) final;
 
     void willSendUpdateGeometry() override;
 
@@ -71,6 +72,8 @@ private:
 
     // The last minimum layout size we sent to the web process.
     WebCore::IntSize m_lastSentMinimumLayoutSize;
+
+    CallbackMap m_callbacks;
 };
 
 } // namespace WebKit
index 4a6f9ea..b9e0073 100644 (file)
@@ -51,6 +51,7 @@ TiledCoreAnimationDrawingAreaProxy::TiledCoreAnimationDrawingAreaProxy(WebPagePr
 
 TiledCoreAnimationDrawingAreaProxy::~TiledCoreAnimationDrawingAreaProxy()
 {
+    m_callbacks.invalidate(CallbackBase::Error::OwnerWasInvalidated);
 }
 
 void TiledCoreAnimationDrawingAreaProxy::deviceScaleFactorDidChange()
@@ -197,10 +198,20 @@ void TiledCoreAnimationDrawingAreaProxy::commitTransientZoom(double scale, Float
 
 void TiledCoreAnimationDrawingAreaProxy::dispatchAfterEnsuringDrawing(WTF::Function<void (CallbackBase::Error)>&& callback)
 {
-    // This callback is primarily used for testing in RemoteLayerTreeDrawingArea. We could in theory wait for a CA commit here.
-    dispatch_async(dispatch_get_main_queue(), BlockPtr<void ()>::fromCallable([callback = WTFMove(callback)] {
-        callback(CallbackBase::Error::None);
-    }).get());
+    if (!m_webPageProxy.isValid()) {
+        callback(CallbackBase::Error::OwnerWasInvalidated);
+        return;
+    }
+
+    m_webPageProxy.process().send(Messages::DrawingArea::AddTransactionCallbackID(m_callbacks.put(WTFMove(callback), nullptr)), m_webPageProxy.pageID());
+}
+
+void TiledCoreAnimationDrawingAreaProxy::dispatchPresentationCallbacksAfterFlushingLayers(const Vector<CallbackID>& callbackIDs)
+{
+    for (auto& callbackID : callbackIDs) {
+        if (auto callback = m_callbacks.take<VoidCallback>(callbackID))
+            callback->performCallback();
+    }
 }
 
 } // namespace WebKit
index a4a7984..00fa7a3 100644 (file)
@@ -159,7 +159,7 @@ bool WebEditorClient::shouldApplyStyle(StyleProperties* style, Range* range)
 
 void WebEditorClient::didApplyStyle()
 {
-    notImplemented();
+    m_page->didApplyStyle();
 }
 
 bool WebEditorClient::shouldMoveRangeAfterDelete(Range*, Range*)
@@ -180,7 +180,7 @@ void WebEditorClient::respondToChangedContents()
 {
     static NeverDestroyed<String> WebViewDidChangeNotification(MAKE_STATIC_STRING_IMPL("WebViewDidChangeNotification"));
     m_page->injectedBundleEditorClient().didChange(*m_page, WebViewDidChangeNotification.get().impl());
-    notImplemented();
+    m_page->didChangeContents();
 }
 
 void WebEditorClient::respondToChangedSelection(Frame* frame)
@@ -197,9 +197,9 @@ void WebEditorClient::respondToChangedSelection(Frame* frame)
 #endif
 }
 
-void WebEditorClient::didChangeSelectionAndUpdateLayout()
+void WebEditorClient::didEndUserTriggeredSelectionChanges()
 {
-    m_page->sendPostLayoutEditorStateIfNeeded();
+    m_page->didEndUserTriggeredSelectionChanges();
 }
 
 void WebEditorClient::updateEditorStateAfterLayoutIfEditabilityChanged()
@@ -207,6 +207,11 @@ void WebEditorClient::updateEditorStateAfterLayoutIfEditabilityChanged()
     m_page->updateEditorStateAfterLayoutIfEditabilityChanged();
 }
 
+void WebEditorClient::didUpdateComposition()
+{
+    m_page->didUpdateComposition();
+}
+
 void WebEditorClient::discardedComposition(Frame*)
 {
     m_page->discardedComposition();
index 613dde2..a607d8e 100644 (file)
@@ -62,10 +62,11 @@ private:
     void didBeginEditing() final;
     void respondToChangedContents() final;
     void respondToChangedSelection(WebCore::Frame*) final;
-    void didChangeSelectionAndUpdateLayout() final;
+    void didEndUserTriggeredSelectionChanges() final;
     void updateEditorStateAfterLayoutIfEditabilityChanged() final;
     void discardedComposition(WebCore::Frame*) final;
     void canceledComposition() final;
+    void didUpdateComposition() final;
     void didEndEditing() final;
     void willWriteSelectionToPasteboard(WebCore::Range*) final;
     void didWriteSelectionToPasteboard() final;
index 699e7b1..e5d072a 100644 (file)
@@ -856,7 +856,8 @@ EditorState WebPage::editorState(IncludePostLayoutDataHint shouldIncludePostLayo
     result.shouldIgnoreSelectionChanges = frame.editor().ignoreSelectionChanges();
 
 #if PLATFORM(COCOA)
-    if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::Yes && result.isContentEditable) {
+    bool canIncludePostLayoutData = frame.view() && !frame.view()->needsLayout();
+    if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::Yes && canIncludePostLayoutData && result.isContentEditable) {
         auto& postLayoutData = result.postLayoutData();
         if (!selection.isNone()) {
             Node* nodeToRemove;
@@ -938,7 +939,7 @@ void WebPage::updateEditorStateAfterLayoutIfEditabilityChanged()
     Frame& frame = m_page->focusController().focusedOrMainFrame();
     EditorStateIsContentEditable editorStateIsContentEditable = frame.selection().selection().isContentEditable() ? EditorStateIsContentEditable::Yes : EditorStateIsContentEditable::No;
     if (m_lastEditorStateWasContentEditable != editorStateIsContentEditable)
-        send(Messages::WebPageProxy::EditorStateChanged(editorState()));
+        sendPartialEditorStateAndSchedulePostLayoutUpdate();
 }
 
 String WebPage::renderTreeExternalRepresentation() const
@@ -3437,6 +3438,11 @@ void WebPage::willCommitLayerTree(RemoteLayerTreeTransaction& layerTransaction)
 #if PLATFORM(MAC)
     layerTransaction.setScrollPosition(frameView->scrollPosition());
 #endif
+
+    if (m_hasPendingEditorStateUpdate) {
+        layerTransaction.setEditorState(editorState());
+        m_hasPendingEditorStateUpdate = false;
+    }
 }
 
 void WebPage::didFlushLayerTreeAtTime(MonotonicTime timestamp)
@@ -5049,6 +5055,16 @@ static bool needsPlainTextQuirk(bool needsQuirks, const URL& url)
 }
 #endif
 
+void WebPage::didApplyStyle()
+{
+    sendEditorStateUpdate();
+}
+
+void WebPage::didChangeContents()
+{
+    sendEditorStateUpdate();
+}
+
 void WebPage::didChangeSelection()
 {
     Frame& frame = m_page->focusController().focusedOrMainFrame();
@@ -5063,14 +5079,6 @@ void WebPage::didChangeSelection()
     if (m_isSelectingTextWhileInsertingAsynchronously)
         return;
 
-    FrameView* view = frame.view();
-
-    // If there is a layout pending, we should avoid populating EditorState that require layout to be done or it will
-    // trigger a synchronous layout every time the selection changes. sendPostLayoutEditorStateIfNeeded() will be called
-    // to send the full editor state after layout is done if we send a partial editor state here.
-    auto editorState = this->editorState(view && view->needsLayout() ? IncludePostLayoutDataHint::No : IncludePostLayoutDataHint::Yes);
-    m_isEditorStateMissingPostLayoutData = editorState.isMissingPostLayoutData;
-
 #if PLATFORM(MAC)
     bool hasPreviouslyFocusedDueToUserInteraction = m_hasEverFocusedElementDueToUserInteractionSincePageTransition;
     m_hasEverFocusedElementDueToUserInteractionSincePageTransition |= m_userIsInteracting;
@@ -5096,15 +5104,11 @@ void WebPage::didChangeSelection()
     if (frame.editor().hasComposition() && !frame.editor().ignoreSelectionChanges() && !frame.selection().isNone()) {
         frame.editor().cancelComposition();
         discardedComposition();
-    } else
-        send(Messages::WebPageProxy::EditorStateChanged(editorState));
-#else
-    send(Messages::WebPageProxy::EditorStateChanged(editorState), pageID(), IPC::SendOption::DispatchMessageEvenWhenWaitingForSyncReply);
+        return;
+    }
 #endif
 
-#if PLATFORM(IOS)
-    m_drawingArea->scheduleCompositingLayerFlush();
-#endif
+    sendPartialEditorStateAndSchedulePostLayoutUpdate();
 }
 
 void WebPage::resetAssistedNodeForFrame(WebFrame* frame)
@@ -5169,24 +5173,28 @@ void WebPage::elementDidBlur(WebCore::Node* node)
     }
 }
 
-void WebPage::sendPostLayoutEditorStateIfNeeded()
+void WebPage::didUpdateComposition()
 {
-    if (!m_isEditorStateMissingPostLayoutData)
-        return;
+    sendEditorStateUpdate();
+}
 
-    send(Messages::WebPageProxy::EditorStateChanged(editorState(IncludePostLayoutDataHint::Yes)), pageID(), IPC::SendOption::DispatchMessageEvenWhenWaitingForSyncReply);
-    m_isEditorStateMissingPostLayoutData = false;
+void WebPage::didEndUserTriggeredSelectionChanges()
+{
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+    if (!frame.editor().ignoreSelectionChanges())
+        sendEditorStateUpdate();
 }
 
 void WebPage::discardedComposition()
 {
     send(Messages::WebPageProxy::CompositionWasCanceled());
-    send(Messages::WebPageProxy::EditorStateChanged(editorState()));
+    sendEditorStateUpdate();
 }
 
 void WebPage::canceledComposition()
 {
     send(Messages::WebPageProxy::CompositionWasCanceled());
+    sendEditorStateUpdate();
 }
 
 void WebPage::setMinimumLayoutSize(const IntSize& minimumLayoutSize)
@@ -5604,6 +5612,54 @@ void WebPage::reportUsedFeatures()
     m_loaderClient->featuresUsedInPage(*this, namedFeatures);
 }
 
+void WebPage::sendEditorStateUpdate()
+{
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+    if (frame.editor().ignoreSelectionChanges())
+        return;
+
+    m_hasPendingEditorStateUpdate = false;
+
+    // If we immediately dispatch an EditorState update to the UI process, layout may not be up to date yet.
+    // If that is the case, just send what we have (i.e. don't include post-layout data) and wait until the
+    // next layer tree commit to compute and send the complete EditorState over.
+    auto state = editorState();
+    send(Messages::WebPageProxy::EditorStateChanged(state), pageID(), IPC::SendOption::DispatchMessageEvenWhenWaitingForSyncReply);
+
+    if (state.isMissingPostLayoutData) {
+        m_hasPendingEditorStateUpdate = true;
+        m_drawingArea->scheduleCompositingLayerFlush();
+    }
+}
+
+void WebPage::sendPartialEditorStateAndSchedulePostLayoutUpdate()
+{
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+    if (frame.editor().ignoreSelectionChanges())
+        return;
+
+    send(Messages::WebPageProxy::EditorStateChanged(editorState(IncludePostLayoutDataHint::No)), pageID(), IPC::SendOption::DispatchMessageEvenWhenWaitingForSyncReply);
+
+    if (m_hasPendingEditorStateUpdate)
+        return;
+
+    // Flag the next layer flush to compute and propagate an EditorState to the UI process.
+    m_hasPendingEditorStateUpdate = true;
+    m_drawingArea->scheduleCompositingLayerFlush();
+}
+
+void WebPage::flushPendingEditorStateUpdate()
+{
+    if (!m_hasPendingEditorStateUpdate)
+        return;
+
+    Frame& frame = m_page->focusController().focusedOrMainFrame();
+    if (frame.editor().ignoreSelectionChanges())
+        return;
+
+    sendEditorStateUpdate();
+}
+
 void WebPage::updateWebsitePolicies(const WebsitePolicies& websitePolicies)
 {
     if (!m_page)
index 4a6bd9e..2ba2916 100644 (file)
@@ -373,7 +373,6 @@ public:
     
     enum class IncludePostLayoutDataHint { No, Yes };
     EditorState editorState(IncludePostLayoutDataHint = IncludePostLayoutDataHint::Yes) const;
-    void sendPostLayoutEditorStateIfNeeded();
     void updateEditorStateAfterLayoutIfEditabilityChanged();
 
     String renderTreeExternalRepresentation() const;
@@ -663,8 +662,11 @@ public:
 
     void didApplyStyle();
     void didChangeSelection();
+    void didChangeContents();
     void discardedComposition();
     void canceledComposition();
+    void didUpdateComposition();
+    void didEndUserTriggeredSelectionChanges();
 
 #if PLATFORM(COCOA)
     void registerUIProcessAccessibilityTokens(const IPC::DataReference& elemenToken, const IPC::DataReference& windowToken);
@@ -985,6 +987,9 @@ public:
 
     static PluginView* pluginViewForFrame(WebCore::Frame*);
 
+    void sendPartialEditorStateAndSchedulePostLayoutUpdate();
+    void flushPendingEditorStateUpdate();
+
 private:
     WebPage(uint64_t pageID, WebPageCreationParameters&&);
 
@@ -998,6 +1003,7 @@ private:
     void platformInitialize();
     void platformDetach();
     void platformEditorState(WebCore::Frame&, EditorState& result, IncludePostLayoutDataHint) const;
+    void sendEditorStateUpdate();
 
     void didReceiveWebPageMessage(IPC::Connection&, IPC::Decoder&);
     void didReceiveSyncWebPageMessage(IPC::Connection&, IPC::Decoder&, std::unique_ptr<IPC::Encoder>&);
@@ -1479,6 +1485,7 @@ private:
 
     RefPtr<WebCore::Node> m_assistedNode;
     bool m_hasPendingBlurNotification { false };
+    bool m_hasPendingEditorStateUpdate { false };
     
 #if PLATFORM(IOS)
     RefPtr<WebCore::Range> m_currentWordRange;
index f641e07..8542681 100644 (file)
@@ -163,7 +163,8 @@ void WebPage::platformEditorState(Frame& frame, EditorState& result, IncludePost
     // entries, we need the layout to be done and we don't want to trigger a synchronous
     // layout as this would be bad for performance. If we have a composition, we send everything
     // right away as the UIProcess needs the caretRects ASAP for marked text.
-    if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::No && !frame.editor().hasComposition()) {
+    bool frameViewHasFinishedLayout = frame.view() && !frame.view()->needsLayout();
+    if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::No && !frameViewHasFinishedLayout && !frame.editor().hasComposition()) {
         result.isMissingPostLayoutData = true;
         return;
     }
@@ -2247,8 +2248,6 @@ void WebPage::applyAutocorrection(const String& correction, const String& origin
 void WebPage::executeEditCommandWithCallback(const String& commandName, CallbackID callbackID)
 {
     executeEditCommand(commandName, String());
-    if (commandName == "toggleBold" || commandName == "toggleItalic" || commandName == "toggleUnderline")
-        send(Messages::WebPageProxy::EditorStateChanged(editorState()));
     send(Messages::WebPageProxy::VoidCallback(callbackID));
 }
 
@@ -3190,6 +3189,26 @@ std::optional<float> WebPage::scaleFromUIProcess(const VisibleContentRectUpdateI
     return scaleFromUIProcess;
 }
 
+static bool selectionIsInsideFixedPositionContainer(Frame& frame)
+{
+    auto& selection = frame.selection().selection();
+    if (selection.isNone())
+        return false;
+
+    bool isInsideFixedPosition = false;
+    if (selection.isCaret()) {
+        frame.selection().absoluteCaretBounds(&isInsideFixedPosition);
+        return isInsideFixedPosition;
+    }
+
+    selection.visibleStart().absoluteCaretBounds(&isInsideFixedPosition);
+    if (isInsideFixedPosition)
+        return true;
+
+    selection.visibleEnd().absoluteCaretBounds(&isInsideFixedPosition);
+    return isInsideFixedPosition;
+}
+
 void WebPage::updateVisibleContentRects(const VisibleContentRectUpdateInfo& visibleContentRectUpdateInfo, MonotonicTime oldestTimestamp)
 {
     LOG_WITH_STREAM(VisibleRects, stream << "\nWebPage::updateVisibleContentRects " << visibleContentRectUpdateInfo);
@@ -3238,7 +3257,8 @@ void WebPage::updateVisibleContentRects(const VisibleContentRectUpdateInfo& visi
         hasSetPageScale = true;
     }
 
-    FrameView& frameView = *m_page->mainFrame().view();
+    auto& frame = m_page->mainFrame();
+    FrameView& frameView = *frame.view();
     if (scrollPosition != frameView.scrollPosition())
         m_dynamicSizeUpdateHistory.clear();
 
@@ -3260,10 +3280,10 @@ void WebPage::updateVisibleContentRects(const VisibleContentRectUpdateInfo& visi
     if (m_isInStableState) {
         if (frameView.frame().settings().visualViewportEnabled()) {
             frameView.setLayoutViewportOverrideRect(LayoutRect(visibleContentRectUpdateInfo.customFixedPositionRect()));
-            const auto& state = editorState();
-            if (!state.isMissingPostLayoutData && state.postLayoutData().insideFixedPosition) {
+            if (selectionIsInsideFixedPositionContainer(frame)) {
+                // Ensure that the next layer tree commit contains up-to-date caret/selection rects.
                 frameView.frame().selection().setCaretRectNeedsUpdate();
-                send(Messages::WebPageProxy::EditorStateChanged(state));
+                sendPartialEditorStateAndSchedulePostLayoutUpdate();
             }
         } else
             frameView.setCustomFixedPositionLayoutRect(enclosingIntRect(visibleContentRectUpdateInfo.customFixedPositionRect()));
index f0e6c55..50c4126 100644 (file)
@@ -28,6 +28,7 @@
 
 #if !PLATFORM(IOS)
 
+#include "CallbackID.h"
 #include "DrawingArea.h"
 #include "LayerTreeContext.h"
 #include <WebCore/FloatRect.h>
@@ -100,6 +101,7 @@ private:
     void setColorSpace(const ColorSpaceData&) override;
     void addFence(const WebCore::MachSendRight&) override;
 
+    void addTransactionCallbackID(CallbackID) override;
     void setShouldScaleViewToFitDocument(bool) override;
 
     void adjustTransientZoom(double scale, WebCore::FloatPoint origin) override;
@@ -160,6 +162,7 @@ private:
     WebCore::IntSize m_lastDocumentSizeForScaleToFit;
 
     WebCore::LayoutMilestones m_pendingNewlyReachedLayoutMilestones { 0 };
+    Vector<CallbackID> m_pendingCallbackIDs;
 };
 
 } // namespace WebKit
index 702dd7f..d1ab442 100644 (file)
@@ -403,6 +403,12 @@ void TiledCoreAnimationDrawingArea::sendPendingNewlyReachedLayoutMilestones()
     m_pendingNewlyReachedLayoutMilestones = 0;
 }
 
+void TiledCoreAnimationDrawingArea::addTransactionCallbackID(CallbackID callbackID)
+{
+    m_pendingCallbackIDs.append(callbackID);
+    scheduleCompositingLayerFlush();
+}
+
 bool TiledCoreAnimationDrawingArea::flushLayers()
 {
     ASSERT(!m_layerTreeStateIsFrozen);
@@ -411,6 +417,7 @@ bool TiledCoreAnimationDrawingArea::flushLayers()
         scaleViewToFitDocumentIfNeeded();
 
         m_webPage.layoutIfNeeded();
+        m_webPage.flushPendingEditorStateUpdate();
 
         updateIntrinsicContentSizeIfNeeded();
 
@@ -448,6 +455,11 @@ bool TiledCoreAnimationDrawingArea::flushLayers()
         if (m_transientZoomScale != 1)
             applyTransientZoomToLayers(m_transientZoomScale, m_transientZoomOrigin);
 
+        if (!m_pendingCallbackIDs.isEmpty()) {
+            m_webPage.send(Messages::DrawingAreaProxy::DispatchPresentationCallbacksAfterFlushingLayers(m_pendingCallbackIDs));
+            m_pendingCallbackIDs.clear();
+        }
+
         return returnValue;
     }
 }
index 0192ff1..3f8d5af 100644 (file)
@@ -125,7 +125,7 @@ void WebPage::platformDetach()
 
 void WebPage::platformEditorState(Frame& frame, EditorState& result, IncludePostLayoutDataHint shouldIncludePostLayoutData) const
 {
-    if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::No || !result.isContentEditable) {
+    if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::No || !frame.view() || frame.view()->needsLayout() || !result.isContentEditable) {
         result.isMissingPostLayoutData = true;
         return;
     }
index b16ccee..ce17605 100644 (file)
@@ -1,3 +1,15 @@
+2017-08-22  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+        https://bugs.webkit.org/show_bug.cgi?id=175370
+        <rdar://problem/33799806>
+
+        Reviewed by Ryosuke Niwa.
+
+        Adjust WebEditorClient for interface changes.
+
+        * WebCoreSupport/WebEditorClient.h:
+
 2017-08-22  Alex Christensen  <achristensen@webkit.org>
 
         Remove ChromeClient::scrollbarsModeDidChange
index dcc950e..328394d 100644 (file)
@@ -110,10 +110,11 @@ private:
 
     void respondToChangedContents() final;
     void respondToChangedSelection(WebCore::Frame*) final;
-    void didChangeSelectionAndUpdateLayout() final { }
+    void didEndUserTriggeredSelectionChanges() final { }
     void updateEditorStateAfterLayoutIfEditabilityChanged() final;
     void discardedComposition(WebCore::Frame*) final;
     void canceledComposition() final;
+    void didUpdateComposition() final { }
 
     void registerUndoStep(WebCore::UndoStep&) final;
     void registerRedoStep(WebCore::UndoStep&) final;
index 97b5c80..3dfa2a8 100644 (file)
@@ -1,3 +1,15 @@
+2017-08-22  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+        https://bugs.webkit.org/show_bug.cgi?id=175370
+        <rdar://problem/33799806>
+
+        Reviewed by Ryosuke Niwa.
+
+        Adjust WebEditorClient for interface changes.
+
+        * WebCoreSupport/WebEditorClient.h:
+
 2017-08-22  Brent Fulgham  <bfulgham@apple.com>
 
         Unreviewed build fix after r221017.
index 673db5f..d516ceb 100644 (file)
@@ -57,10 +57,10 @@ private:
 
     void respondToChangedContents() final;
     void respondToChangedSelection(WebCore::Frame*) final;
-    void didChangeSelectionAndUpdateLayout() final { }
     void updateEditorStateAfterLayoutIfEditabilityChanged() final { } 
     void canceledComposition() final;
     void discardedComposition(WebCore::Frame*) final;
+    void didUpdateComposition() final { }
 
     bool shouldDeleteRange(WebCore::Range*) final;
 
index 740a43a..bd9ad0f 100644 (file)
@@ -1,3 +1,32 @@
+2017-08-22  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+        https://bugs.webkit.org/show_bug.cgi?id=175370
+        <rdar://problem/33799806>
+
+        Reviewed by Ryosuke Niwa.
+
+        Tweaks API tests that involve editing to wait for a presentation update before checking against UI process-side
+        information sent via EditorState updates. This allows any EditorState update scheduled by the test to propagate
+        to the UI process.
+
+        * TestWebKitAPI/Tests/WebKit2Cocoa/WKWebViewCandidateTests.mm:
+        (-[CandidateTestWebView typeString:inputMessage:]):
+        (+[CandidateTestWebView setUpWithFrame:testPage:]):
+        * TestWebKitAPI/Tests/WebKit2Cocoa/WKWebViewTextInput.mm:
+        * TestWebKitAPI/Tests/mac/AcceptsFirstMouse.mm:
+        (TestWebKitAPI::AcceptsFirstMouse::runTest):
+        * TestWebKitAPI/Tests/mac/WKWebViewMacEditingTests.mm:
+        * TestWebKitAPI/cocoa/TestWKWebView.h:
+        * TestWebKitAPI/cocoa/TestWKWebView.mm:
+        (-[TestWKWebView waitForNextPresentationUpdate]):
+
+        Add a new helper method to spin until the next presentation update.
+
+        * TestWebKitAPI/mac/WebKitAgnosticTest.h:
+        * TestWebKitAPI/mac/WebKitAgnosticTest.mm:
+        (TestWebKitAPI::WebKitAgnosticTest::waitForNextPresentationUpdate):
+
 2017-08-22  Alex Christensen  <achristensen@webkit.org>
 
         Add UIDelegatePrivate SPI corresponding to WKPageUIClient.showPage
index 57b5145..b18cc94 100644 (file)
@@ -121,6 +121,7 @@ static NSString *GetDocumentScrollTopJSExpression = @"document.body.scrollTop";
             [self typeCharacter:[string characterAtIndex:i]];
         });
         [self waitForMessage:inputMessage];
+        [self waitForNextPresentationUpdate];
     }
 }
 
@@ -130,6 +131,7 @@ static NSString *GetDocumentScrollTopJSExpression = @"document.body.scrollTop";
 
     [wkWebView loadTestPageNamed:testPageName];
     [wkWebView waitForMessage:@"focused"];
+    [wkWebView waitForNextPresentationUpdate];
     [wkWebView _forceRequestCandidates];
 
     return wkWebView;
@@ -142,13 +144,13 @@ TEST(WKWebViewCandidateTests, SoftSpaceReplacementAfterCandidateInsertionWithout
     CandidateTestWebView *wkWebView = [CandidateTestWebView setUpWithFrame:NSMakeRect(0, 0, 800, 600) testPage:@"input-field-in-scrollable-document"];
 
     [wkWebView insertCandidatesAndWaitForResponse:@"apple " range:NSMakeRange(0, 0)];
-    EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"apple "]);
+    EXPECT_WK_STREQ("apple ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
 
     [wkWebView expectCandidateListVisibilityUpdates:0 whenPerformingActions:^()
     {
         [wkWebView typeString:@" " inputMessage:@"input"];
     }];
-    EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"apple "]);
+    EXPECT_WK_STREQ("apple ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
     EXPECT_EQ([[wkWebView stringByEvaluatingJavaScript:GetDocumentScrollTopJSExpression] doubleValue], 0);
 }
 
@@ -157,10 +159,10 @@ TEST(WKWebViewCandidateTests, InsertCharactersAfterCandidateInsertionWithSoftSpa
     CandidateTestWebView *wkWebView = [CandidateTestWebView setUpWithFrame:NSMakeRect(0, 0, 800, 600) testPage:@"input-field-in-scrollable-document"];
 
     [wkWebView insertCandidatesAndWaitForResponse:@"foo " range:NSMakeRange(0, 0)];
-    EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"foo "]);
+    EXPECT_WK_STREQ("foo ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
 
     [wkWebView typeString:@"a" inputMessage:@"input"];
-    EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"foo a"]);
+    EXPECT_WK_STREQ("foo a", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
 }
 
 TEST(WKWebViewCandidateTests, InsertCandidateFromPartiallyTypedPhraseWithSoftSpace)
@@ -169,16 +171,16 @@ TEST(WKWebViewCandidateTests, InsertCandidateFromPartiallyTypedPhraseWithSoftSpa
 
     [wkWebView typeString:@"hel" inputMessage:@"input"];
     [wkWebView insertCandidatesAndWaitForResponse:@"hello " range:NSMakeRange(0, 3)];
-    EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"hello "]);
+    EXPECT_WK_STREQ("hello ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
 
     [wkWebView expectCandidateListVisibilityUpdates:0 whenPerformingActions:^()
     {
         [wkWebView typeString:@" " inputMessage:@"input"];
-        EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"hello "]);
+        EXPECT_WK_STREQ("hello ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
         EXPECT_EQ([[wkWebView stringByEvaluatingJavaScript:GetDocumentScrollTopJSExpression] doubleValue], 0);
 
         [wkWebView typeString:@" " inputMessage:@"input"];
-        EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"hello  "]);
+        EXPECT_WK_STREQ("hello  ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
         EXPECT_EQ([[wkWebView stringByEvaluatingJavaScript:GetDocumentScrollTopJSExpression] doubleValue], 0);
     }];
 }
@@ -196,7 +198,7 @@ TEST(WKWebViewCandidateTests, ClickingInTextFieldDoesNotThrashCandidateVisibilit
         });
         [wkWebView waitForMessage:@"mousedown"];
     }];
-    EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"test"]);
+    EXPECT_WK_STREQ("test", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
 }
 
 TEST(WKWebViewCandidateTests, ShouldNotRequestCandidatesInPasswordField)
index 53ab229..288a455 100644 (file)
 #import "PlatformUtilities.h"
 #import "Test.h"
 #import "TestNavigationDelegate.h"
+#import "TestWKWebView.h"
 #import <WebKit/WebKit.h>
 #import <wtf/RetainPtr.h>
 
 TEST(WKWebView, ShouldHaveInputContextForEditableContent)
 {
-    RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
-
-    NSURLRequest *request = [NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"editable-body" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]];
-    [webView loadRequest:request];
-    [webView _test_waitForDidFinishNavigation];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
+    [webView synchronouslyLoadTestPageNamed:@"editable-body"];
+    [webView waitForNextPresentationUpdate];
 
     EXPECT_NOT_NULL([webView inputContext]);
 }
index 7c34494..fa61985 100644 (file)
@@ -50,6 +50,7 @@ void AcceptsFirstMouse::runTest(View view)
 {
     RetainPtr<NSWindow> window = adoptNS([[NSWindow alloc] initWithContentRect:view.frame styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:YES]);
     [[window.get() contentView] addSubview:view];
+    waitForNextPresentationUpdate(view);
 
     CGFloat viewHeight = view.bounds.size.height;
 
index 00607c3..342564f 100644 (file)
@@ -124,6 +124,7 @@ TEST(WKWebViewMacEditingTests, DoNotCrashWhenInterpretingKeyEventWhileDeallocati
         SlowInputWebView *webView = [[[SlowInputWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)] autorelease];
         [webView synchronouslyLoadHTMLString:[NSString stringWithFormat:@"<body contenteditable>Hello world</body>"]];
         [webView stringByEvaluatingJavaScript:@"document.body.focus()"];
+        [webView waitForNextPresentationUpdate];
         [webView removeFromSuperview];
         [webView typeCharacter:'a'];
 
index 2497ebd..51da3db 100644 (file)
@@ -48,6 +48,7 @@
 - (NSString *)stringByEvaluatingJavaScript:(NSString *)script;
 - (void)waitForMessage:(NSString *)message;
 - (void)performAfterLoading:(dispatch_block_t)actions;
+- (void)waitForNextPresentationUpdate;
 @end
 
 #if PLATFORM(IOS)
index 8d4adc7..b756ca1 100644 (file)
@@ -274,6 +274,16 @@ NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
     [contentController addScriptMessageHandler:handler name:@"onloadHandler"];
 }
 
+- (void)waitForNextPresentationUpdate
+{
+    __block bool done = false;
+    [self _doAfterNextPresentationUpdate:^() {
+        done = true;
+    }];
+
+    TestWebKitAPI::Util::run(&done);
+}
+
 @end
 
 #if PLATFORM(IOS)
index c25c4b4..f6c963a 100644 (file)
@@ -46,6 +46,9 @@ public:
     void goBack(WebView *);
     void goBack(WKView *);
 
+    void waitForNextPresentationUpdate(WebView *);
+    void waitForNextPresentationUpdate(WKView *);
+
     void waitForLoadToFinish();
 
     NSRect viewFrame;
index 127fe51..2dcd32d 100644 (file)
@@ -139,4 +139,23 @@ void WebKitAgnosticTest::waitForLoadToFinish()
     didFinishLoad = false;
 }
 
+void WebKitAgnosticTest::waitForNextPresentationUpdate(WebView *)
+{
+    // FIXME: This isn't currently required anywhere. Just dispatch to the next runloop for now.
+    __block bool done = false;
+    dispatch_async(dispatch_get_main_queue(), ^() {
+        done = true;
+    });
+    Util::run(&done);
+}
+
+void WebKitAgnosticTest::waitForNextPresentationUpdate(WKView *view)
+{
+    __block bool done = false;
+    [view _doAfterNextPresentationUpdate:^() {
+        done = true;
+    }];
+    Util::run(&done);
+}
+
 } // namespace TestWebKitAPI