[iOS WK2] WKWebView schedules nonstop layout after pressing cmb+b,i,u inside a conten...
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 8 Aug 2017 08:10:23 +0000 (08:10 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 8 Aug 2017 08:10:23 +0000 (08:10 +0000)
https://bugs.webkit.org/show_bug.cgi?id=175116
<rdar://problem/28279301>

Reviewed by Darin Adler and Ryosuke Niwa.

Source/WebCore:

WebCore support for WebPage::editorState refactoring. See WebKit ChangeLogs for more detail.

Tests: EditorStateTests.TypingAttributesBold
       EditorStateTests.TypingAttributesItalic
       EditorStateTests.TypingAttributesUnderline
       EditorStateTests.TypingAttributesTextAlignmentAbsoluteAlignmentOptions
       EditorStateTests.TypingAttributesTextAlignmentStartEnd
       EditorStateTests.TypingAttributesTextAlignmentDirectionalText
       EditorStateTests.TypingAttributesTextColor
       EditorStateTests.TypingAttributesMixedStyles
       EditorStateTests.TypingAttributesLinkColor

* css/StyleProperties.cpp:
(WebCore::StyleProperties::propertyAsColor const):
(WebCore::StyleProperties::propertyAsValueID const):

Introduces some helper functions in StyleProperties to convert CSS property values to Color or a CSSValueID.

* css/StyleProperties.h:
* editing/EditingStyle.cpp:
(WebCore::EditingStyle::hasStyle):

Pull out logic in selectionStartHasStyle that asks for a style TriState into EditingStyle::hasStyle. This is
because WebPage::editorState will now query for multiple styles at the selection start, but
selectionStartHasStyle currently recomputes styleAtSelectionStart every time it is called. To prevent extra work
from being done, we can just call selectionStartHasStyle once and use ask for EditingStyle::hasStyle on the
computed EditingStyle at selection start.

* editing/EditingStyle.h:
* editing/Editor.cpp:
(WebCore::Editor::selectionStartHasStyle const):

Source/WebKit:

Refactors WebPage::editorState to only use the StyleProperties derived from EditingStyle, instead of inserting
and removing a temporary node to figure out the style. Also adds hooks to notify the UI delegate of EditorState
changes.

* UIProcess/API/Cocoa/WKUIDelegatePrivate.h:
* UIProcess/API/Cocoa/WKWebView.mm:
(nsTextAlignment):
(dictionaryRepresentationForEditorState):
(-[WKWebView _didChangeEditorState]):

Alerts the private UI delegate of UI-side EditorState updates.

(-[WKWebView _web_editorStateDidChange]):
(-[WKWebView _executeEditCommand:argument:completion:]):
* UIProcess/API/Cocoa/WKWebViewInternal.h:
* UIProcess/API/Cocoa/WKWebViewPrivate.h:
* UIProcess/API/mac/WKView.mm:
(-[WKView _web_editorStateDidChange]):
* UIProcess/Cocoa/WebViewImpl.h:
* UIProcess/Cocoa/WebViewImpl.mm:
(WebKit::WebViewImpl::selectionDidChange):
* UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::executeEditCommand):

Change executeEditCommand(name, callback) to executeEditCommand(name, argument, callback) and lift out of iOS
platform code and into WebPage.cpp.

* UIProcess/WebPageProxy.h:
* UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView executeEditCommandWithCallback:]):
(-[WKContentView _selectionChanged]):
* UIProcess/ios/WebPageProxyIOS.mm:
(WebKit::WebPageProxy::executeEditCommand): Deleted.

Move the iOS-specific implementation of executeEditCommand that invokes a callback when the web process responds
out of WebPageProxyIOS, and into cross-platform WebPageProxy code. Additionally, add a parameter for the edit
command's argument.

* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::editorState const):

Use EditingStyle::styleAtSelectionStart instead of Editor::styleForSelectionStart when computing an EditorState.
Tweak bold, italic and underline to use EditingStyle TriStates.

(WebKit::shouldEnsureEditorStateUpdateAfterExecutingCommand):
(WebKit::WebPage::executeEditCommandWithCallback):
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/WebPage.messages.in:
* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::WebPage::executeEditCommandWithCallback): Deleted.

Tools:

Introduces new testing infrastructure and API tests to test EditorState updates in the UI process. The new
EditorStateTests run on both iOS and Mac.

* TestWebKitAPI/EditingTestHarness.h: Added.
* TestWebKitAPI/EditingTestHarness.mm: Added.

EditingTestHarness is a helper object that API tests may use to apply editing commands and store EditorState
history. This test harness adds sugaring around various editing commands, and simplifies the process of checking
the state of the latest observed EditorState.

(-[EditingTestHarness initWithWebView:]):
(-[EditingTestHarness dealloc]):
(-[EditingTestHarness webView]):
(-[EditingTestHarness latestEditorState]):
(-[EditingTestHarness editorStateHistory]):
(-[EditingTestHarness insertText:andExpectEditorStateWith:]):
(-[EditingTestHarness insertHTML:andExpectEditorStateWith:]):
(-[EditingTestHarness selectAllAndExpectEditorStateWith:]):
(-[EditingTestHarness moveBackwardAndExpectEditorStateWith:]):
(-[EditingTestHarness moveWordBackwardAndExpectEditorStateWith:]):
(-[EditingTestHarness toggleBold]):
(-[EditingTestHarness toggleItalic]):
(-[EditingTestHarness toggleUnderline]):
(-[EditingTestHarness setForegroundColor:]):
(-[EditingTestHarness alignJustifiedAndExpectEditorStateWith:]):
(-[EditingTestHarness alignLeftAndExpectEditorStateWith:]):
(-[EditingTestHarness alignCenterAndExpectEditorStateWith:]):
(-[EditingTestHarness alignRightAndExpectEditorStateWith:]):
(-[EditingTestHarness insertParagraphAndExpectEditorStateWith:]):
(-[EditingTestHarness deleteBackwardAndExpectEditorStateWith:]):
(-[EditingTestHarness _execCommand:argument:expectEntries:]):

Dispatches an editing command to the web process, and blocks until a response is received. If an expected
entries dictionary is given, this will additionally verify that the latest EditorState contains all the expected
keys and values.

(-[EditingTestHarness latestEditorStateContains:]):
(-[EditingTestHarness _webView:editorStateDidChange:]):
* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/WebKit2Cocoa/EditorStateTests.mm: Added.
(TestWebKitAPI::setUpEditorStateTestHarness):
(TestWebKitAPI::TEST):
* TestWebKitAPI/Tests/WebKit2Cocoa/editor-state-test-harness.html: Added.

LayoutTests:

Rebaseline some iOS WK2 LayoutTest expectations. These tests currently expect an empty anonymous RenderBlock to
be inserted into the render tree, but this is only a result of us adding and removing a temporary <span> when
computing a RenderStyle in WebPage::editorState -- this patch removes these empty RenderBlocks, making these
expectations' RenderTrees consistent with WebKit1.

* 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:
* platform/ios-wk2/editing/style/unbold-in-bold-expected.txt:

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

33 files changed:
LayoutTests/ChangeLog
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/ios-wk2/editing/style/unbold-in-bold-expected.txt
Source/WebCore/ChangeLog
Source/WebCore/css/StyleProperties.cpp
Source/WebCore/css/StyleProperties.h
Source/WebCore/editing/EditingStyle.cpp
Source/WebCore/editing/EditingStyle.h
Source/WebCore/editing/Editor.cpp
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/API/Cocoa/WKUIDelegatePrivate.h
Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
Source/WebKit/UIProcess/API/Cocoa/WKWebViewInternal.h
Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h
Source/WebKit/UIProcess/API/mac/WKView.mm
Source/WebKit/UIProcess/Cocoa/WebViewImpl.h
Source/WebKit/UIProcess/Cocoa/WebViewImpl.mm
Source/WebKit/UIProcess/WebPageProxy.cpp
Source/WebKit/UIProcess/WebPageProxy.h
Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Source/WebKit/UIProcess/ios/WebPageProxyIOS.mm
Source/WebKit/WebProcess/WebPage/WebPage.cpp
Source/WebKit/WebProcess/WebPage/WebPage.h
Source/WebKit/WebProcess/WebPage/WebPage.messages.in
Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
Tools/ChangeLog
Tools/TestWebKitAPI/EditingTestHarness.h [new file with mode: 0644]
Tools/TestWebKitAPI/EditingTestHarness.mm [new file with mode: 0644]
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WebKit2Cocoa/EditorStateTests.mm [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/WebKit2Cocoa/editor-state-test-harness.html [new file with mode: 0644]

index eab36131bca343703705da3ec5a9767eb4910b51..16383c4447976646e828e07e5e8bbfe39520cb63 100644 (file)
@@ -1,3 +1,21 @@
+2017-08-08  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS WK2] WKWebView schedules nonstop layout after pressing cmb+b,i,u inside a contenteditable div
+        https://bugs.webkit.org/show_bug.cgi?id=175116
+        <rdar://problem/28279301>
+
+        Reviewed by Darin Adler and Ryosuke Niwa.
+
+        Rebaseline some iOS WK2 LayoutTest expectations. These tests currently expect an empty anonymous RenderBlock to
+        be inserted into the render tree, but this is only a result of us adding and removing a temporary <span> when
+        computing a RenderStyle in WebPage::editorState -- this patch removes these empty RenderBlocks, making these
+        expectations' RenderTrees consistent with WebKit1.
+
+        * 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:
+        * platform/ios-wk2/editing/style/unbold-in-bold-expected.txt:
+
 2017-08-07  Matt Lewis  <jlewis3@apple.com>
 
         Marked media/modern-media-controls/fullscreen-support/fullscreen-support-press.html as flaky.
index 1e41ca5bdb98a2f0f86d0df58118945b7840c11c..491323d9c269112103dc3954bd8f687574f6aba2 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 a61cffed4f780fc839d93d6e801e9e690959b9cc..d00e802060142aa239a3112fabd84127caa50a5b 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 1a0cf5cfc18c1e68e9bbb19a92260c51b8d3f28f..a794a68a3ca7467678de957f0c71d1e47f39812f 100644 (file)
@@ -14,5 +14,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 2 {DIV} of body
index b1ba240ba7764c3b5f2f02f3672264d6e4950750..092e5bdd07b5569fab4dc61e7f04fcbe4661d14c 100644 (file)
@@ -86,6 +86,5 @@ layer at (0,0) size 800x600
           RenderText {#text} at (170,15) size 72x28
             text run at (170,15) width 72: "xxxxxx"
         RenderInline {SPAN} at (0,0) size 0x28
-      RenderBlock (anonymous) at (0,58) size 784x0
 selection start: position 0 of child 1 {#text} of child 1 {DIV} of body
 selection end:   position 7 of child 1 {#text} of child 1 {DIV} of body
index 13d6118be82ca11d8277a48fed1ab46146efff86..141296ad8b1140fc1dfbdb64e5d42c57d5b352fa 100644 (file)
@@ -1,3 +1,43 @@
+2017-08-08  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS WK2] WKWebView schedules nonstop layout after pressing cmb+b,i,u inside a contenteditable div
+        https://bugs.webkit.org/show_bug.cgi?id=175116
+        <rdar://problem/28279301>
+
+        Reviewed by Darin Adler and Ryosuke Niwa.
+
+        WebCore support for WebPage::editorState refactoring. See WebKit ChangeLogs for more detail.
+
+        Tests: EditorStateTests.TypingAttributesBold
+               EditorStateTests.TypingAttributesItalic
+               EditorStateTests.TypingAttributesUnderline
+               EditorStateTests.TypingAttributesTextAlignmentAbsoluteAlignmentOptions
+               EditorStateTests.TypingAttributesTextAlignmentStartEnd
+               EditorStateTests.TypingAttributesTextAlignmentDirectionalText
+               EditorStateTests.TypingAttributesTextColor
+               EditorStateTests.TypingAttributesMixedStyles
+               EditorStateTests.TypingAttributesLinkColor
+
+        * css/StyleProperties.cpp:
+        (WebCore::StyleProperties::propertyAsColor const):
+        (WebCore::StyleProperties::propertyAsValueID const):
+
+        Introduces some helper functions in StyleProperties to convert CSS property values to Color or a CSSValueID.
+
+        * css/StyleProperties.h:
+        * editing/EditingStyle.cpp:
+        (WebCore::EditingStyle::hasStyle):
+
+        Pull out logic in selectionStartHasStyle that asks for a style TriState into EditingStyle::hasStyle. This is
+        because WebPage::editorState will now query for multiple styles at the selection start, but
+        selectionStartHasStyle currently recomputes styleAtSelectionStart every time it is called. To prevent extra work
+        from being done, we can just call selectionStartHasStyle once and use ask for EditingStyle::hasStyle on the
+        computed EditingStyle at selection start.
+
+        * editing/EditingStyle.h:
+        * editing/Editor.cpp:
+        (WebCore::Editor::selectionStartHasStyle const):
+
 2017-08-08  Zan Dobersek  <zdobersek@igalia.com>
 
         [TexMap] Add TextureMapperContextAttributes
index 0e9311851d1303f5456a846219ced8ad278d8fd8..4cf4a50579413403db9663c2d5e7dd55cada3a4d 100644 (file)
@@ -31,6 +31,7 @@
 #include "CSSValueKeywords.h"
 #include "CSSValueList.h"
 #include "CSSValuePool.h"
+#include "Color.h"
 #include "Document.h"
 #include "PropertySetCSSStyleDeclaration.h"
 #include "StylePropertyShorthand.h"
@@ -239,6 +240,22 @@ String StyleProperties::getPropertyValue(CSSPropertyID propertyID) const
     }
 }
 
+std::optional<Color> StyleProperties::propertyAsColor(CSSPropertyID property) const
+{
+    auto colorValue = getPropertyCSSValue(property);
+    if (!is<CSSPrimitiveValue>(colorValue.get()))
+        return std::nullopt;
+
+    auto& primitiveColor = downcast<CSSPrimitiveValue>(*colorValue);
+    return primitiveColor.isRGBColor() ? primitiveColor.color() : CSSParser::parseColor(colorValue->cssText());
+}
+
+CSSValueID StyleProperties::propertyAsValueID(CSSPropertyID property) const
+{
+    auto cssValue = getPropertyCSSValue(property);
+    return is<CSSPrimitiveValue>(cssValue.get()) ? downcast<CSSPrimitiveValue>(*cssValue).valueID() : CSSValueInvalid;
+}
+
 String StyleProperties::getCustomPropertyValue(const String& propertyName) const
 {
     RefPtr<CSSValue> value = getCustomPropertyCSSValue(propertyName);
index d01d6294683ba82426057a3fdba70c3f5a97908e..2661ec1121108a635065dc6a250145d6d5649a39 100644 (file)
@@ -36,6 +36,7 @@ namespace WebCore {
 class CSSDeferredParser;
 class CSSStyleDeclaration;
 class CachedResource;
+class Color;
 class ImmutableStyleProperties;
 class URL;
 class MutableStyleProperties;
@@ -112,6 +113,10 @@ public:
 
     WEBCORE_EXPORT RefPtr<CSSValue> getPropertyCSSValue(CSSPropertyID) const;
     WEBCORE_EXPORT String getPropertyValue(CSSPropertyID) const;
+
+    WEBCORE_EXPORT std::optional<Color> propertyAsColor(CSSPropertyID) const;
+    WEBCORE_EXPORT CSSValueID propertyAsValueID(CSSPropertyID) const;
+
     bool propertyIsImportant(CSSPropertyID) const;
     String getPropertyShorthand(CSSPropertyID) const;
     bool isPropertyImplicit(CSSPropertyID) const;
index e34aa78d0f63295cfc35fdd6bc17aee31139a98b..bbd6ab238e3b91c78e71dbc99b756774a9a294f6 100644 (file)
@@ -1413,6 +1413,11 @@ int EditingStyle::legacyFontSize(Document* document) const
         m_shouldUseFixedDefaultFontSize, AlwaysUseLegacyFontSize);
 }
 
+bool EditingStyle::hasStyle(CSSPropertyID propertyID, const String& value)
+{
+    return EditingStyle::create(propertyID, value)->triStateOfStyle(this) != FalseTriState;
+}
+
 RefPtr<EditingStyle> EditingStyle::styleAtSelectionStart(const VisibleSelection& selection, bool shouldUseBackgroundColorInEffect)
 {
     if (selection.isNone())
index c17138d4017d2e5aa274e5845697a934b7ba6568..596c08ee2a58369e39b24879b9294c37dbba6070 100644 (file)
@@ -165,6 +165,7 @@ public:
     void setStrikeThroughChange(TextDecorationChange change) { m_strikeThroughChange = static_cast<unsigned>(change); }
     TextDecorationChange strikeThroughChange() const { return static_cast<TextDecorationChange>(m_strikeThroughChange); }
 
+    WEBCORE_EXPORT bool hasStyle(CSSPropertyID, const String& value);
     WEBCORE_EXPORT static RefPtr<EditingStyle> styleAtSelectionStart(const VisibleSelection&, bool shouldUseBackgroundColorInEffect = false);
     static WritingDirection textDirectionForSelection(const VisibleSelection&, EditingStyle* typingStyle, bool& hasNestedOrMultipleEmbeddings);
 
index 9924403665af5de3d4282a17422a22ec2cd6b9af..99543efe659c2a911419c36978d1b882beb0244f 100644 (file)
@@ -916,8 +916,9 @@ void Editor::applyParagraphStyleToSelection(StyleProperties* style, EditAction e
 
 bool Editor::selectionStartHasStyle(CSSPropertyID propertyID, const String& value) const
 {
-    return EditingStyle::create(propertyID, value)->triStateOfStyle(
-        EditingStyle::styleAtSelectionStart(m_frame.selection().selection(), propertyID == CSSPropertyBackgroundColor).get());
+    if (auto editingStyle = EditingStyle::styleAtSelectionStart(m_frame.selection().selection(), propertyID == CSSPropertyBackgroundColor))
+        return editingStyle->hasStyle(propertyID, value);
+    return false;
 }
 
 TriState Editor::selectionHasStyle(CSSPropertyID propertyID, const String& value) const
index f9581e4a2da574f5b5beaeaf5efa3ebbbefee664..e5feadae740fbf016bf11e1e685ba4afcc928851 100644 (file)
@@ -1,3 +1,62 @@
+2017-08-08  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS WK2] WKWebView schedules nonstop layout after pressing cmb+b,i,u inside a contenteditable div
+        https://bugs.webkit.org/show_bug.cgi?id=175116
+        <rdar://problem/28279301>
+
+        Reviewed by Darin Adler and Ryosuke Niwa.
+
+        Refactors WebPage::editorState to only use the StyleProperties derived from EditingStyle, instead of inserting
+        and removing a temporary node to figure out the style. Also adds hooks to notify the UI delegate of EditorState
+        changes.
+
+        * UIProcess/API/Cocoa/WKUIDelegatePrivate.h:
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (nsTextAlignment):
+        (dictionaryRepresentationForEditorState):
+        (-[WKWebView _didChangeEditorState]):
+
+        Alerts the private UI delegate of UI-side EditorState updates.
+
+        (-[WKWebView _web_editorStateDidChange]):
+        (-[WKWebView _executeEditCommand:argument:completion:]):
+        * UIProcess/API/Cocoa/WKWebViewInternal.h:
+        * UIProcess/API/Cocoa/WKWebViewPrivate.h:
+        * UIProcess/API/mac/WKView.mm:
+        (-[WKView _web_editorStateDidChange]):
+        * UIProcess/Cocoa/WebViewImpl.h:
+        * UIProcess/Cocoa/WebViewImpl.mm:
+        (WebKit::WebViewImpl::selectionDidChange):
+        * UIProcess/WebPageProxy.cpp:
+        (WebKit::WebPageProxy::executeEditCommand):
+
+        Change executeEditCommand(name, callback) to executeEditCommand(name, argument, callback) and lift out of iOS
+        platform code and into WebPage.cpp.
+
+        * UIProcess/WebPageProxy.h:
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (-[WKContentView executeEditCommandWithCallback:]):
+        (-[WKContentView _selectionChanged]):
+        * UIProcess/ios/WebPageProxyIOS.mm:
+        (WebKit::WebPageProxy::executeEditCommand): Deleted.
+
+        Move the iOS-specific implementation of executeEditCommand that invokes a callback when the web process responds
+        out of WebPageProxyIOS, and into cross-platform WebPageProxy code. Additionally, add a parameter for the edit
+        command's argument.
+
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::WebPage::editorState const):
+
+        Use EditingStyle::styleAtSelectionStart instead of Editor::styleForSelectionStart when computing an EditorState.
+        Tweak bold, italic and underline to use EditingStyle TriStates.
+
+        (WebKit::shouldEnsureEditorStateUpdateAfterExecutingCommand):
+        (WebKit::WebPage::executeEditCommandWithCallback):
+        * WebProcess/WebPage/WebPage.h:
+        * WebProcess/WebPage/WebPage.messages.in:
+        * WebProcess/WebPage/ios/WebPageIOS.mm:
+        (WebKit::WebPage::executeEditCommandWithCallback): Deleted.
+
 2017-08-08  Zan Dobersek  <zdobersek@igalia.com>
 
         [TexMap] Don't expose GraphicsContext3D object
index f4bb532a82ac261aaa8edf4fc72629057912edc1..25cd6c90831a2f0d329699df871c49d1cc5fb948 100644 (file)
@@ -78,6 +78,7 @@ struct UIEdgeInsets;
 - (void)_webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures completionHandler:(void (^)(WKWebView *webView))completionHandler WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));
 
 - (void)_webView:(WKWebView *)webView runBeforeUnloadConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));
+- (void)_webView:(WKWebView *)webView editorStateDidChange:(NSDictionary *)editorState WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));
 
 #if TARGET_OS_IPHONE
 - (BOOL)_webView:(WKWebView *)webView shouldIncludeAppLinkActionsForElement:(_WKActivatedElementInfo *)element WK_API_AVAILABLE(ios(9.0));
index c72f9e78056b6da945de13da98d9ec9e2fbf1049..0d83f81169e99ade96231e4404c4ac088ba27481 100644 (file)
@@ -1085,6 +1085,47 @@ static WKErrorCode callbackErrorCode(WebKit::CallbackBase::Error error)
     _page->setViewportSizeForCSSViewportUnits(viewportSizeForViewportUnits);
 }
 
+static NSTextAlignment nsTextAlignment(WebKit::TextAlignment alignment)
+{
+    switch (alignment) {
+    case WebKit::NoAlignment:
+        return NSTextAlignmentNatural;
+    case WebKit::LeftAlignment:
+        return NSTextAlignmentLeft;
+    case WebKit::RightAlignment:
+        return NSTextAlignmentRight;
+    case WebKit::CenterAlignment:
+        return NSTextAlignmentCenter;
+    case WebKit::JustifiedAlignment:
+        return NSTextAlignmentJustified;
+    }
+    ASSERT_NOT_REACHED();
+    return NSTextAlignmentNatural;
+}
+
+static NSDictionary *dictionaryRepresentationForEditorState(const WebKit::EditorState& state)
+{
+    if (state.isMissingPostLayoutData)
+        return @{ @"post-layout-data" : @NO };
+
+    auto& postLayoutData = state.postLayoutData();
+    return @{
+        @"post-layout-data" : @YES,
+        @"bold": postLayoutData.typingAttributes & WebKit::AttributeBold ? @YES : @NO,
+        @"italic": postLayoutData.typingAttributes & WebKit::AttributeItalics ? @YES : @NO,
+        @"underline": postLayoutData.typingAttributes & WebKit::AttributeUnderline ? @YES : @NO,
+        @"text-alignment": @(nsTextAlignment(static_cast<WebKit::TextAlignment>(postLayoutData.textAlignment))),
+        @"text-color": (NSString *)postLayoutData.textColor.cssText()
+    };
+}
+
+- (void)_didChangeEditorState
+{
+    id <WKUIDelegatePrivate> uiDelegate = (id <WKUIDelegatePrivate>)self.UIDelegate;
+    if ([uiDelegate respondsToSelector:@selector(_webView:editorStateDidChange:)])
+        [uiDelegate _webView:self editorStateDidChange:dictionaryRepresentationForEditorState(_page->editorState())];
+}
+
 #pragma mark iOS-specific methods
 
 #if PLATFORM(IOS)
@@ -3639,6 +3680,11 @@ WEBCORE_COMMAND(yankAndSelect)
     _impl->dismissContentRelativeChildWindowsWithAnimationFromViewOnly(withAnimation);
 }
 
+- (void)_web_editorStateDidChange
+{
+    [self _didChangeEditorState];
+}
+
 - (void)_web_gestureEventWasNotHandledByWebCore:(NSEvent *)event
 {
     _impl->gestureEventWasNotHandledByWebCoreFromViewOnly(event);
@@ -5655,6 +5701,13 @@ static WebCore::UserInterfaceLayoutDirection toUserInterfaceLayoutDirection(UISe
     WebKit::ViewSnapshotStore::singleton().setDisableSnapshotVolatilityForTesting(true);
 }
 
+- (void)_executeEditCommand:(NSString *)command argument:(NSString *)argument completion:(void (^)(BOOL))completion
+{
+    _page->executeEditCommand(command, argument, [capturedCompletionBlock = makeBlockPtr(completion)](WebKit::CallbackBase::Error error) {
+        capturedCompletionBlock(error == WebKit::CallbackBase::Error::None);
+    });
+}
+
 #if PLATFORM(IOS)
 
 - (void)_simulateDataInteractionEntered:(id)info
index 8b8713ace1857ae2aae13be1d31be459e550c91e..7ebe8e0a507207491352894a93504cc66463792a 100644 (file)
@@ -119,6 +119,8 @@ struct PrintInfo;
 - (void)_showPasswordViewWithDocumentName:(NSString *)documentName passwordHandler:(void (^)(NSString *))passwordHandler;
 - (void)_hidePasswordView;
 
+- (void)_didChangeEditorState;
+
 - (void)_addShortcut:(id)sender;
 - (void)_arrowKey:(id)sender;
 - (void)_define:(id)sender;
index c50fbd4fcb2a4c4e1caa0daee7736c430b3d007a..016f9492334ce363ba05920624a05fe1c0667536 100644 (file)
@@ -407,6 +407,7 @@ typedef NS_ENUM(NSInteger, _WKImmediateActionType) {
 - (void)_doAfterNextVisibleContentRectUpdate:(void (^)(void))updateBlock WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));
 
 - (void)_disableBackForwardSnapshotVolatilityForTesting WK_API_AVAILABLE(macosx(10.12.3), ios(10.3));
+- (void)_executeEditCommand:(NSString *)command argument:(NSString *)argument completion:(void (^)(BOOL))completion WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));
 
 @end
 
index e2ff3487082d2a29d7d79e8b93fa4aa01061bd13..0e6662016cafb89fa35fe4a165465bd3e84f2f73 100644 (file)
@@ -1029,6 +1029,10 @@ Some other editing-related methods still unimplemented:
     [self _dismissContentRelativeChildWindowsWithAnimation:withAnimation];
 }
 
+- (void)_web_editorStateDidChange
+{
+}
+
 - (void)_web_gestureEventWasNotHandledByWebCore:(NSEvent *)event
 {
     [self _gestureEventWasNotHandledByWebCore:event];
index 4f80af23b524a99e71cec0a8b450caf9771dc798..d4133b464940276df0d1a420fc2d517394063ac2 100644 (file)
@@ -85,6 +85,7 @@ OBJC_CLASS WebPlaybackControlsManager;
 
 - (void)_web_dismissContentRelativeChildWindows;
 - (void)_web_dismissContentRelativeChildWindowsWithAnimation:(BOOL)animate;
+- (void)_web_editorStateDidChange;
 
 - (void)_web_gestureEventWasNotHandledByWebCore:(NSEvent *)event;
 
index 9aede70ae17137faa123cb50e8e63849a5e22d85..cacb856d5a3a0561076adab5a052f8c0b3bd56f3 100644 (file)
@@ -2610,6 +2610,8 @@ void WebViewImpl::selectionDidChange()
     if (!m_page->editorState().isMissingPostLayoutData)
         requestCandidatesForSelectionIfNeeded();
 #endif
+
+    [m_view _web_editorStateDidChange];
 }
 
 void WebViewImpl::didBecomeEditable()
index 8b03472420192c88e36eb71ac628dbe786c998ff..e7e042c170d80169602f5df0c2ff0b4e5945b2be 100644 (file)
@@ -1642,6 +1642,17 @@ void WebPageProxy::setMaintainsInactiveSelection(bool newValue)
 {
     m_maintainsInactiveSelection = newValue;
 }
+
+void WebPageProxy::executeEditCommand(const String& commandName, const String& argument, WTF::Function<void(CallbackBase::Error)>&& callbackFunction)
+{
+    if (!isValid()) {
+        callbackFunction(CallbackBase::Error::Unknown);
+        return;
+    }
+
+    auto callbackID = m_callbacks.put(WTFMove(callbackFunction), m_process->throttler().backgroundActivityToken());
+    m_process->send(Messages::WebPage::ExecuteEditCommandWithCallback(commandName, argument, callbackID), m_pageID);
+}
     
 void WebPageProxy::executeEditCommand(const String& commandName, const String& argument)
 {
index 644ed75c483d5dfde3a96ea3ce4d11f0a7817cd0..274d32acf2b5e6d133f1ab30807f8d082ef60c76 100644 (file)
@@ -479,9 +479,9 @@ public:
     void activateMediaStreamCaptureInPage();
     bool isMediaStreamCaptureMuted() const { return m_mutedState & WebCore::MediaProducer::CaptureDevicesAreMuted; }
     void setMediaStreamCaptureMuted(bool);
+    void executeEditCommand(const String& commandName, const String& argument, WTF::Function<void(CallbackBase::Error)>&&);
         
 #if PLATFORM(IOS)
-    void executeEditCommand(const String& commandName, WTF::Function<void (CallbackBase::Error)>&&);
     double displayedContentScale() const { return m_lastVisibleContentRectUpdate.scale(); }
     const WebCore::FloatRect& exposedContentRect() const { return m_lastVisibleContentRectUpdate.exposedContentRect(); }
     const WebCore::FloatRect& unobscuredContentRect() const { return m_lastVisibleContentRectUpdate.unobscuredContentRect(); }
index 94ea37477dc988bb0fe0ac404143bf5ffd62ab1c..239af7166f72008983c8da107156fd89b98bea07 100644 (file)
@@ -3571,7 +3571,7 @@ static NSString *contentTypeFromFieldName(WebCore::AutofillFieldName fieldName)
 {
     [self beginSelectionChange];
     RetainPtr<WKContentView> view = self;
-    _page->executeEditCommand(commandName, [view](WebKit::CallbackBase::Error) {
+    _page->executeEditCommand(commandName, { }, [view](WebKit::CallbackBase::Error) {
         [view endSelectionChange];
     });
 }
@@ -3923,6 +3923,8 @@ static bool isAssistableInputType(InputType type)
     // to wait to paint the selection.
     if (_usingGestureForSelection)
         [self _updateChangedSelection];
+
+    [_webView _didChangeEditorState];
 }
 
 - (void)selectWordForReplacement
index 7845aa9d96a3e653229ce41d5e3ed3e39bd73a59..0ea7815a8da985a86be2084916c9a62d6fd1cd99 100644 (file)
@@ -476,17 +476,6 @@ void WebPageProxy::applyAutocorrection(const String& correction, const String& o
     m_process->send(Messages::WebPage::ApplyAutocorrection(correction, originalText, callbackID), m_pageID);
 }
 
-void WebPageProxy::executeEditCommand(const String& commandName, WTF::Function<void (CallbackBase::Error)>&& callbackFunction)
-{
-    if (!isValid()) {
-        callbackFunction(CallbackBase::Error::Unknown);
-        return;
-    }
-    
-    auto callbackID = m_callbacks.put(WTFMove(callbackFunction), m_process->throttler().backgroundActivityToken());
-    m_process->send(Messages::WebPage::ExecuteEditCommandWithCallback(commandName, callbackID), m_pageID);
-}
-
 bool WebPageProxy::applyAutocorrection(const String& correction, const String& originalText)
 {
     bool autocorrectionApplied = false;
index 60ae396296ee879b58d9dc42636db0c46d4db440..16217de01a6e8de37808cf8e4a7087f002680842 100644 (file)
@@ -861,63 +861,55 @@ EditorState WebPage::editorState(IncludePostLayoutDataHint shouldIncludePostLayo
     if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::Yes && result.isContentEditable) {
         auto& postLayoutData = result.postLayoutData();
         if (!selection.isNone()) {
-            Node* nodeToRemove;
-            if (auto* style = Editor::styleForSelectionStart(&frame, nodeToRemove)) {
-                if (isFontWeightBold(style->fontCascade().weight()))
+            if (auto editingStyle = EditingStyle::styleAtSelectionStart(selection)) {
+                if (editingStyle->hasStyle(CSSPropertyFontWeight, "bold"))
                     postLayoutData.typingAttributes |= AttributeBold;
-                if (isItalic(style->fontCascade().italic()))
+
+                if (editingStyle->hasStyle(CSSPropertyFontStyle, "italic") || editingStyle->hasStyle(CSSPropertyFontStyle, "oblique"))
                     postLayoutData.typingAttributes |= AttributeItalics;
 
-                RefPtr<EditingStyle> typingStyle = frame.selection().typingStyle();
-                if (typingStyle && typingStyle->style()) {
-                    String value = typingStyle->style()->getPropertyValue(CSSPropertyWebkitTextDecorationsInEffect);
-                if (value.contains("underline"))
+                if (editingStyle->hasStyle(CSSPropertyWebkitTextDecorationsInEffect, "underline"))
                     postLayoutData.typingAttributes |= AttributeUnderline;
-                } else {
-                    if (style->textDecorationsInEffect() & TextDecorationUnderline)
-                        postLayoutData.typingAttributes |= AttributeUnderline;
-                }
-
-                if (style->visitedDependentColor(CSSPropertyColor).isValid())
-                    postLayoutData.textColor = style->visitedDependentColor(CSSPropertyColor);
 
-                switch (style->textAlign()) {
-                case RIGHT:
-                case WEBKIT_RIGHT:
-                    postLayoutData.textAlignment = RightAlignment;
-                    break;
-                case LEFT:
-                case WEBKIT_LEFT:
-                    postLayoutData.textAlignment = LeftAlignment;
-                    break;
-                case CENTER:
-                case WEBKIT_CENTER:
-                    postLayoutData.textAlignment = CenterAlignment;
-                    break;
-                case JUSTIFY:
-                    postLayoutData.textAlignment = JustifiedAlignment;
-                    break;
-                case TASTART:
-                    postLayoutData.textAlignment = style->isLeftToRightDirection() ? LeftAlignment : RightAlignment;
-                    break;
-                case TAEND:
-                    postLayoutData.textAlignment = style->isLeftToRightDirection() ? RightAlignment : LeftAlignment;
-                    break;
+                if (auto* styleProperties = editingStyle->style()) {
+                    bool isLeftToRight = styleProperties->propertyAsValueID(CSSPropertyDirection) == CSSValueLtr;
+                    switch (styleProperties->propertyAsValueID(CSSPropertyTextAlign)) {
+                    case CSSValueRight:
+                    case CSSValueWebkitRight:
+                        postLayoutData.textAlignment = RightAlignment;
+                        break;
+                    case CSSValueLeft:
+                    case CSSValueWebkitLeft:
+                        postLayoutData.textAlignment = LeftAlignment;
+                        break;
+                    case CSSValueCenter:
+                    case CSSValueWebkitCenter:
+                        postLayoutData.textAlignment = CenterAlignment;
+                        break;
+                    case CSSValueJustify:
+                        postLayoutData.textAlignment = JustifiedAlignment;
+                        break;
+                    case CSSValueStart:
+                        postLayoutData.textAlignment = isLeftToRight ? LeftAlignment : RightAlignment;
+                        break;
+                    case CSSValueEnd:
+                        postLayoutData.textAlignment = isLeftToRight ? RightAlignment : LeftAlignment;
+                        break;
+                    default:
+                        break;
+                    }
+                    if (auto textColor = styleProperties->propertyAsColor(CSSPropertyColor))
+                        postLayoutData.textColor = *textColor;
                 }
-                
-                HTMLElement* enclosingListElement = enclosingList(selection.start().deprecatedNode());
-                if (enclosingListElement) {
-                    if (is<HTMLUListElement>(*enclosingListElement))
-                        postLayoutData.enclosingListType = UnorderedList;
-                    else if (is<HTMLOListElement>(*enclosingListElement))
-                        postLayoutData.enclosingListType = OrderedList;
-                    else
-                        ASSERT_NOT_REACHED();
-                } else
-                    postLayoutData.enclosingListType = NoList;
-
-                if (nodeToRemove)
-                    nodeToRemove->remove();
+            }
+
+            if (auto* enclosingListElement = enclosingList(selection.start().containerNode())) {
+                if (is<HTMLUListElement>(*enclosingListElement))
+                    postLayoutData.enclosingListType = UnorderedList;
+                else if (is<HTMLOListElement>(*enclosingListElement))
+                    postLayoutData.enclosingListType = OrderedList;
+                else
+                    ASSERT_NOT_REACHED();
             }
         }
     }
@@ -930,6 +922,21 @@ EditorState WebPage::editorState(IncludePostLayoutDataHint shouldIncludePostLayo
     return result;
 }
 
+static bool shouldEnsureEditorStateUpdateAfterExecutingCommand(const String& commandName)
+{
+    // These commands will always ensure an EditorState update in the UI process.
+    // FIXME: This logic was moved here from iOS platform-specific code; we should investigate whether this makes sense for all platforms.
+    return commandName == "toggleBold" || commandName == "toggleItalic" || commandName == "toggleUnderline";
+}
+
+void WebPage::executeEditCommandWithCallback(const String& commandName, const String& argument, CallbackID callbackID)
+{
+    executeEditCommand(commandName, argument);
+    if (shouldEnsureEditorStateUpdateAfterExecutingCommand(commandName))
+        send(Messages::WebPageProxy::EditorStateChanged(editorState()));
+    send(Messages::WebPageProxy::VoidCallback(callbackID));
+}
+
 void WebPage::updateEditorStateAfterLayoutIfEditabilityChanged()
 {
     // FIXME: We should update EditorStateIsContentEditable to track whether the state is richly
index 6bc371e20fc4979ffc7241035ba46c0f65deb419..f8d263155f10c9fc1ece8e26be1c0050476e24fb 100644 (file)
@@ -516,6 +516,7 @@ public:
     void resetAssistedNodeForFrame(WebFrame*);
 
     void viewportPropertiesDidChange(const WebCore::ViewportArguments&);
+    void executeEditCommandWithCallback(const String&, const String& argument, CallbackID);
 
 #if PLATFORM(IOS)
     WebCore::FloatSize screenSize() const;
@@ -587,7 +588,6 @@ public:
 #endif
 
     void contentSizeCategoryDidChange(const String&);
-    void executeEditCommandWithCallback(const String&, CallbackID);
 
     Seconds eventThrottlingDelay() const;
 
index 8e4d0c68d406dbbe33525cd911140ab54a726c8d..9c71066192dcaf0afd3845c21c837feef302c17d 100644 (file)
@@ -37,6 +37,7 @@ messages -> WebPage LegacyReceiver {
     ViewWillStartLiveResize()
     ViewWillEndLiveResize()
 
+    ExecuteEditCommandWithCallback(String name, String argument, WebKit::CallbackID callbackID)
     KeyEvent(WebKit::WebKeyboardEvent event)
     MouseEvent(WebKit::WebMouseEvent event)
 #if PLATFORM(IOS)
@@ -91,7 +92,6 @@ messages -> WebPage LegacyReceiver {
     ApplicationWillEnterForeground(bool isSuspendedUnderLock)
     ApplicationDidBecomeActive()
     ContentSizeCategoryDidChange(String contentSizeCategory)
-    ExecuteEditCommandWithCallback(String name, WebKit::CallbackID callbackID)
     GetSelectionContext(WebKit::CallbackID callbackID)
     SetAllowsMediaDocumentInlinePlayback(bool allows)
     HandleTwoFingerTapAtPoint(WebCore::IntPoint point, uint64_t requestID)
index 2f15fb17a64962786f9caea43f5d87bac86eb674..65dc20369e34f02288c0fc71678e26bd3997c43b 100644 (file)
@@ -2245,14 +2245,6 @@ void WebPage::applyAutocorrection(const String& correction, const String& origin
     send(Messages::WebPageProxy::StringCallback(correctionApplied ? correction : String(), callbackID));
 }
 
-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));
-}
-
 Seconds WebPage::eventThrottlingDelay() const
 {
     auto behaviorOverride = m_page->eventThrottlingBehaviorOverride();
index 0617def36bd76a75ee6444f5d5d512972557119c..bdd30199f59adca2f9e154af40d44cdc4af93df7 100644 (file)
@@ -1,3 +1,55 @@
+2017-08-08  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS WK2] WKWebView schedules nonstop layout after pressing cmb+b,i,u inside a contenteditable div
+        https://bugs.webkit.org/show_bug.cgi?id=175116
+        <rdar://problem/28279301>
+
+        Reviewed by Darin Adler and Ryosuke Niwa.
+
+        Introduces new testing infrastructure and API tests to test EditorState updates in the UI process. The new
+        EditorStateTests run on both iOS and Mac.
+
+        * TestWebKitAPI/EditingTestHarness.h: Added.
+        * TestWebKitAPI/EditingTestHarness.mm: Added.
+
+        EditingTestHarness is a helper object that API tests may use to apply editing commands and store EditorState
+        history. This test harness adds sugaring around various editing commands, and simplifies the process of checking
+        the state of the latest observed EditorState.
+
+        (-[EditingTestHarness initWithWebView:]):
+        (-[EditingTestHarness dealloc]):
+        (-[EditingTestHarness webView]):
+        (-[EditingTestHarness latestEditorState]):
+        (-[EditingTestHarness editorStateHistory]):
+        (-[EditingTestHarness insertText:andExpectEditorStateWith:]):
+        (-[EditingTestHarness insertHTML:andExpectEditorStateWith:]):
+        (-[EditingTestHarness selectAllAndExpectEditorStateWith:]):
+        (-[EditingTestHarness moveBackwardAndExpectEditorStateWith:]):
+        (-[EditingTestHarness moveWordBackwardAndExpectEditorStateWith:]):
+        (-[EditingTestHarness toggleBold]):
+        (-[EditingTestHarness toggleItalic]):
+        (-[EditingTestHarness toggleUnderline]):
+        (-[EditingTestHarness setForegroundColor:]):
+        (-[EditingTestHarness alignJustifiedAndExpectEditorStateWith:]):
+        (-[EditingTestHarness alignLeftAndExpectEditorStateWith:]):
+        (-[EditingTestHarness alignCenterAndExpectEditorStateWith:]):
+        (-[EditingTestHarness alignRightAndExpectEditorStateWith:]):
+        (-[EditingTestHarness insertParagraphAndExpectEditorStateWith:]):
+        (-[EditingTestHarness deleteBackwardAndExpectEditorStateWith:]):
+        (-[EditingTestHarness _execCommand:argument:expectEntries:]):
+
+        Dispatches an editing command to the web process, and blocks until a response is received. If an expected
+        entries dictionary is given, this will additionally verify that the latest EditorState contains all the expected
+        keys and values.
+
+        (-[EditingTestHarness latestEditorStateContains:]):
+        (-[EditingTestHarness _webView:editorStateDidChange:]):
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/WebKit2Cocoa/EditorStateTests.mm: Added.
+        (TestWebKitAPI::setUpEditorStateTestHarness):
+        (TestWebKitAPI::TEST):
+        * TestWebKitAPI/Tests/WebKit2Cocoa/editor-state-test-harness.html: Added.
+
 2017-08-04  Brent Fulgham  <bfulgham@apple.com>
 
         Prevent domain from being set to a TLD
diff --git a/Tools/TestWebKitAPI/EditingTestHarness.h b/Tools/TestWebKitAPI/EditingTestHarness.h
new file mode 100644 (file)
index 0000000..f05b6b2
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#if WK_API_ENABLED
+
+#import "TestWKWebView.h"
+#import <WebKit/WKUIDelegatePrivate.h>
+
+@interface EditingTestHarness : NSObject<WKUIDelegatePrivate> {
+    RetainPtr<NSMutableArray<NSDictionary *> *> _editorStateHistory;
+    RetainPtr<TestWKWebView *> _webView;
+}
+
+- (instancetype)initWithWebView:(TestWKWebView *)webView;
+
+@property (nonatomic, readonly) TestWKWebView *webView;
+@property (nonatomic, readonly) NSDictionary *latestEditorState;
+@property (nonatomic, readonly) NSArray<NSDictionary *> *editorStateHistory;
+
+- (void)insertParagraphAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries;
+- (void)insertText:(NSString *)text andExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries;
+- (void)insertHTML:(NSString *)html andExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries;
+- (void)selectAllAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries;
+- (void)moveBackwardAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries;
+- (void)moveWordBackwardAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries;
+- (void)deleteBackwardAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries;
+- (void)toggleBold;
+- (void)toggleItalic;
+- (void)toggleUnderline;
+- (void)setForegroundColor:(NSString *)colorAsString;
+- (void)alignJustifiedAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries;
+- (void)alignLeftAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries;
+- (void)alignCenterAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries;
+- (void)alignRightAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries;
+
+- (BOOL)latestEditorStateContains:(NSDictionary<NSString *, id> *)entries;
+
+@end
+
+#endif // WK_API_ENABLED
diff --git a/Tools/TestWebKitAPI/EditingTestHarness.mm b/Tools/TestWebKitAPI/EditingTestHarness.mm
new file mode 100644 (file)
index 0000000..0bbd445
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+
+#include "config.h"
+#include "EditingTestHarness.h"
+
+#if WK_API_ENABLED
+
+#import "PlatformUtilities.h"
+#import <WebKit/WKWebViewPrivate.h>
+
+@implementation EditingTestHarness
+
+- (instancetype)initWithWebView:(TestWKWebView *)webView
+{
+    if (self = [super init]) {
+        _webView = webView;
+        [_webView setUIDelegate:self];
+        _editorStateHistory = adoptNS([[NSMutableArray alloc] init]);
+    }
+    return self;
+}
+
+- (void)dealloc
+{
+    if ([_webView UIDelegate] == self)
+        [_webView setUIDelegate:nil];
+
+    [super dealloc];
+}
+
+- (TestWKWebView *)webView
+{
+    return _webView.get();
+}
+
+- (NSDictionary *)latestEditorState
+{
+    return self.editorStateHistory.lastObject;
+}
+
+- (NSArray<NSDictionary *> *)editorStateHistory
+{
+    return _editorStateHistory.get();
+}
+
+- (void)insertText:(NSString *)text andExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries
+{
+    [self _execCommand:@"InsertText" argument:text expectEntries:entries];
+}
+
+- (void)insertHTML:(NSString *)html andExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries
+{
+    [self _execCommand:@"InsertHTML" argument:html expectEntries:entries];
+}
+
+- (void)selectAllAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries
+{
+    [self _execCommand:@"SelectAll" argument:nil expectEntries:entries];
+}
+
+- (void)moveBackwardAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries
+{
+    [self _execCommand:@"MoveBackward" argument:nil expectEntries:entries];
+}
+
+- (void)moveWordBackwardAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries
+{
+    [self _execCommand:@"MoveWordBackward" argument:nil expectEntries:entries];
+}
+
+- (void)toggleBold
+{
+    [self _execCommand:@"ToggleBold" argument:nil expectEntries:nil];
+}
+
+- (void)toggleItalic
+{
+    [self _execCommand:@"ToggleItalic" argument:nil expectEntries:nil];
+}
+
+- (void)toggleUnderline
+{
+    [self _execCommand:@"ToggleUnderline" argument:nil expectEntries:nil];
+}
+
+- (void)setForegroundColor:(NSString *)colorAsString
+{
+    [self _execCommand:@"ForeColor" argument:colorAsString expectEntries:nil];
+}
+
+- (void)alignJustifiedAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries
+{
+    [self _execCommand:@"AlignJustified" argument:nil expectEntries:entries];
+}
+
+- (void)alignLeftAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries
+{
+    [self _execCommand:@"AlignLeft" argument:nil expectEntries:entries];
+}
+
+- (void)alignCenterAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries
+{
+    [self _execCommand:@"AlignCenter" argument:nil expectEntries:entries];
+}
+
+- (void)alignRightAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries
+{
+    [self _execCommand:@"AlignRight" argument:nil expectEntries:entries];
+}
+
+- (void)insertParagraphAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries
+{
+    [self _execCommand:@"InsertParagraph" argument:nil expectEntries:entries];
+}
+
+- (void)deleteBackwardAndExpectEditorStateWith:(NSDictionary<NSString *, id> *)entries
+{
+    [self _execCommand:@"DeleteBackward" argument:nil expectEntries:entries];
+}
+
+- (void)_execCommand:(NSString *)command argument:(NSString *)argument expectEntries:(NSDictionary<NSString *, id> *)entries
+{
+    __block BOOL result = false;
+    __block bool done = false;
+    [_webView _executeEditCommand:command argument:argument completion:^(BOOL success) {
+        result = success;
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    EXPECT_TRUE(result);
+    if (!result)
+        NSLog(@"Failed to execute editing command: ('%@', '%@')", command, argument ?: @"");
+
+    BOOL containsEntries = [self latestEditorStateContains:entries];
+    EXPECT_TRUE(containsEntries);
+    if (!containsEntries)
+        NSLog(@"Expected %@ to contain %@", self.latestEditorState, entries);
+}
+
+- (BOOL)latestEditorStateContains:(NSDictionary<NSString *, id> *)entries
+{
+    NSDictionary *latestEditorState = self.latestEditorState;
+    for (NSString *key in entries) {
+        if (![latestEditorState[key] isEqual:entries[key]])
+            return NO;
+    }
+    return latestEditorState.count || !entries.count;
+}
+
+#pragma mark - WKUIDelegatePrivate
+
+- (void)_webView:(WKWebView *)webView editorStateDidChange:(NSDictionary *)editorState
+{
+    if (![editorState[@"post-layout-data"] boolValue])
+        return;
+
+    if (![self.latestEditorState isEqualToDictionary:editorState])
+        [_editorStateHistory addObject:editorState];
+}
+
+@end
+
+#endif // WK_API_ENABLED
index dd2ed9b3b284954ceefbf39e76633242fa34ef91..9cde63689565a00eac3b946d03d0c3cdfc65423e 100644 (file)
                F41AB9AA1EF4696B0083FA08 /* textarea-to-input.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F41AB9951EF4692C0083FA08 /* textarea-to-input.html */; };
                F42DA5161D8CEFE400336F40 /* large-input-field-focus-onload.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F42DA5151D8CEFDB00336F40 /* large-input-field-focus-onload.html */; };
                F4451C761EB8FD890020C5DA /* two-paragraph-contenteditable.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4451C751EB8FD7C0020C5DA /* two-paragraph-contenteditable.html */; };
+               F44D06451F395C26001A0E29 /* editor-state-test-harness.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F44D06441F395C0D001A0E29 /* editor-state-test-harness.html */; };
+               F44D06471F39627A001A0E29 /* EditorStateTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F44D06461F395C4D001A0E29 /* EditorStateTests.mm */; };
+               F44D064A1F3962F2001A0E29 /* EditingTestHarness.mm in Sources */ = {isa = PBXBuildFile; fileRef = F44D06491F3962E3001A0E29 /* EditingTestHarness.mm */; };
                F4538EF71E8473E600B5C953 /* large-red-square.png in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4538EF01E846B4100B5C953 /* large-red-square.png */; };
                F45B63FB1F197F4A009D38B9 /* image-map.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F45B63FA1F197F33009D38B9 /* image-map.html */; };
                F45B63FE1F19D410009D38B9 /* ActionSheetTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F45B63FC1F19D410009D38B9 /* ActionSheetTests.mm */; };
                                F4D5E4E81F0C5D38008C1A49 /* dragstart-clear-selection.html in Copy Resources */,
                                A155022C1E050D0300A24C57 /* duplicate-completion-handler-calls.html in Copy Resources */,
                                9984FACE1CFFB090008D198C /* editable-body.html in Copy Resources */,
+                               F44D06451F395C26001A0E29 /* editor-state-test-harness.html in Copy Resources */,
                                51C8E1A91F27F49600BF731B /* EmptyGrandfatheredResourceLoadStatistics.plist in Copy Resources */,
                                A14AAB651E78DC5400C1ADC2 /* encrypted.pdf in Copy Resources */,
                                F4C2AB221DD6D95E00E06D5B /* enormous-video-with-sound.html in Copy Resources */,
                F41AB99E1EF4692C0083FA08 /* div-and-large-image.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "div-and-large-image.html"; sourceTree = "<group>"; };
                F42DA5151D8CEFDB00336F40 /* large-input-field-focus-onload.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = "large-input-field-focus-onload.html"; path = "Tests/WebKit2Cocoa/large-input-field-focus-onload.html"; sourceTree = SOURCE_ROOT; };
                F4451C751EB8FD7C0020C5DA /* two-paragraph-contenteditable.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "two-paragraph-contenteditable.html"; sourceTree = "<group>"; };
+               F44D06441F395C0D001A0E29 /* editor-state-test-harness.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "editor-state-test-harness.html"; sourceTree = "<group>"; };
+               F44D06461F395C4D001A0E29 /* EditorStateTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EditorStateTests.mm; sourceTree = "<group>"; };
+               F44D06481F3962E3001A0E29 /* EditingTestHarness.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EditingTestHarness.h; sourceTree = "<group>"; };
+               F44D06491F3962E3001A0E29 /* EditingTestHarness.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EditingTestHarness.mm; sourceTree = "<group>"; };
                F4538EF01E846B4100B5C953 /* large-red-square.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "large-red-square.png"; sourceTree = "<group>"; };
                F45B63FA1F197F33009D38B9 /* image-map.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "image-map.html"; sourceTree = "<group>"; };
                F45B63FC1F19D410009D38B9 /* ActionSheetTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ActionSheetTests.mm; sourceTree = "<group>"; };
                        isa = PBXGroup;
                        children = (
                                A13EBB441B87332B00097110 /* WebProcessPlugIn */,
+                               F44D06481F3962E3001A0E29 /* EditingTestHarness.h */,
+                               F44D06491F3962E3001A0E29 /* EditingTestHarness.mm */,
                                5C726D6D1D3EE06800C5E1A1 /* InstanceMethodSwizzler.h */,
                                5C726D6E1D3EE06800C5E1A1 /* InstanceMethodSwizzler.mm */,
                                0F139E721A423A2B00F590F5 /* PlatformUtilitiesCocoa.mm */,
                                2DC60E221E79F88C00FA6C7D /* DoAfterNextPresentationUpdateAfterCrash.mm */,
                                A1A4FE5D18DD3DB700B5EA8A /* Download.mm */,
                                A15502281E05020B00A24C57 /* DuplicateCompletionHandlerCalls.mm */,
+                               F44D06461F395C4D001A0E29 /* EditorStateTests.mm */,
                                2D8104CB1BEC13E70020DA46 /* FindInPage.mm */,
                                2D1FE0AF1AD465C1006CD9E6 /* FixedLayoutSize.mm */,
                                CD78E11A1DB7EA360014A2DE /* FullscreenDelegate.mm */,
                                F4D5E4E71F0C5D27008C1A49 /* dragstart-clear-selection.html */,
                                A155022B1E050BC500A24C57 /* duplicate-completion-handler-calls.html */,
                                9984FACD1CFFB038008D198C /* editable-body.html */,
+                               F44D06441F395C0D001A0E29 /* editor-state-test-harness.html */,
                                51C8E1A81F27F47300BF731B /* EmptyGrandfatheredResourceLoadStatistics.plist */,
                                F4C2AB211DD6D94100E06D5B /* enormous-video-with-sound.html */,
                                F407FE381F1D0DE60017CF25 /* enormous.svg */,
                                A155022A1E05020B00A24C57 /* DuplicateCompletionHandlerCalls.mm in Sources */,
                                7CCE7EBE1A411A7E00447C4C /* DynamicDeviceScaleFactor.mm in Sources */,
                                5C0BF8921DD599B600B00328 /* EarlyKVOCrash.mm in Sources */,
+                               F44D064A1F3962F2001A0E29 /* EditingTestHarness.mm in Sources */,
                                7CCE7EE01A411A9A00447C4C /* EditorCommands.mm in Sources */,
+                               F44D06471F39627A001A0E29 /* EditorStateTests.mm in Sources */,
                                7CCE7EBF1A411A7E00447C4C /* ElementAtPointInWebFrame.mm in Sources */,
                                07492B3B1DF8B14C00633DE1 /* EnumerateMediaDevices.cpp in Sources */,
                                448D7E471EA6C55500ECC756 /* EnvironmentUtilitiesTest.cpp in Sources */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKit2Cocoa/EditorStateTests.mm b/Tools/TestWebKitAPI/Tests/WebKit2Cocoa/EditorStateTests.mm
new file mode 100644 (file)
index 0000000..0aad003
--- /dev/null
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#if WK_API_ENABLED
+
+#import "EditingTestHarness.h"
+#import "PlatformUtilities.h"
+#import "TestWKWebView.h"
+#import <WebKit/WKWebViewPrivate.h>
+
+namespace TestWebKitAPI {
+
+static RetainPtr<EditingTestHarness> setUpEditorStateTestHarness()
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
+    auto testHarness = adoptNS([[EditingTestHarness alloc] initWithWebView:webView.get()]);
+    [webView synchronouslyLoadTestPageNamed:@"editor-state-test-harness"];
+    return testHarness;
+}
+
+TEST(EditorStateTests, TypingAttributesBold)
+{
+    auto testHarness = setUpEditorStateTestHarness();
+
+    [testHarness insertHTML:@"<b>first</b>" andExpectEditorStateWith:@{ @"bold": @YES }];
+    [testHarness toggleBold];
+    [testHarness insertText:@" second" andExpectEditorStateWith:@{ @"bold": @NO }];
+    [testHarness insertHTML:@"<span style='font-weight: 700'> third</span>" andExpectEditorStateWith:@{ @"bold": @YES }];
+    [testHarness insertHTML:@"<span style='font-weight: 300'> fourth</span>" andExpectEditorStateWith:@{ @"bold": @NO }];
+    [testHarness insertHTML:@"<span style='font-weight: 800'> fifth</span>" andExpectEditorStateWith:@{ @"bold": @YES }];
+    [testHarness insertHTML:@"<span style='font-weight: 400'> sixth</span>" andExpectEditorStateWith:@{ @"bold": @NO }];
+    [testHarness insertHTML:@"<span style='font-weight: 900'> seventh</span>" andExpectEditorStateWith:@{ @"bold": @YES }];
+    [testHarness toggleBold];
+    [testHarness insertText:@" eighth" andExpectEditorStateWith:@{ @"bold": @NO }];
+    [testHarness insertHTML:@"<strong> ninth</strong>" andExpectEditorStateWith:@{ @"bold": @YES }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"bold": @YES }];
+    [testHarness deleteBackwardAndExpectEditorStateWith:@{ @"bold": @YES }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"bold": @YES }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"bold": @NO }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"bold": @YES }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"bold": @NO }];
+    [testHarness selectAllAndExpectEditorStateWith:@{ @"bold": @YES }];
+    EXPECT_WK_STREQ("first second third fourth fifth sixth seventh eighth ninth", [[testHarness webView] stringByEvaluatingJavaScript:@"getSelection().toString()"]);
+}
+
+TEST(EditorStateTests, TypingAttributesItalic)
+{
+    auto testHarness = setUpEditorStateTestHarness();
+
+    [testHarness insertHTML:@"<i>first</i>" andExpectEditorStateWith:@{ @"italic": @YES }];
+    [testHarness toggleItalic];
+    [testHarness insertText:@" second" andExpectEditorStateWith:@{ @"italic": @NO }];
+    [testHarness insertHTML:@"<span style='font-style: italic'> third</span>" andExpectEditorStateWith:@{ @"italic": @YES }];
+    [testHarness toggleItalic];
+    [testHarness insertText:@" fourth" andExpectEditorStateWith:@{ @"italic": @NO }];
+    [testHarness toggleItalic];
+    [testHarness insertText:@" fifth" andExpectEditorStateWith:@{ @"italic": @YES }];
+    [testHarness insertHTML:@"<span style='font-style: normal'> sixth</span>" andExpectEditorStateWith:@{ @"italic": @NO }];
+    [testHarness insertHTML:@"<span style='font-style: oblique'> seventh</span>" andExpectEditorStateWith:@{ @"italic": @YES }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"italic": @YES }];
+    [testHarness deleteBackwardAndExpectEditorStateWith:@{ @"italic": @YES }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"italic": @YES }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"italic": @NO }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"italic": @YES }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"italic": @NO }];
+
+    [testHarness selectAllAndExpectEditorStateWith:@{ @"italic": @YES }];
+    EXPECT_WK_STREQ("first second third fourth fifth sixth seventh", [[testHarness webView] stringByEvaluatingJavaScript:@"getSelection().toString()"]);
+}
+
+TEST(EditorStateTests, TypingAttributesUnderline)
+{
+    auto testHarness = setUpEditorStateTestHarness();
+
+    [testHarness insertHTML:@"<u>first</u>" andExpectEditorStateWith:@{ @"underline": @YES }];
+    [testHarness toggleUnderline];
+    [testHarness insertText:@" second" andExpectEditorStateWith:@{ @"underline": @NO }];
+    [testHarness insertHTML:@"<span style='text-decoration: underline'> third</span>" andExpectEditorStateWith:@{ @"underline": @YES }];
+    [testHarness insertHTML:@"<span style='text-decoration: line-through'> fourth</span>" andExpectEditorStateWith:@{ @"underline": @NO }];
+    [testHarness insertHTML:@"<span style='text-decoration: underline overline line-through'> fifth</span>" andExpectEditorStateWith:@{ @"underline": @YES }];
+    [testHarness insertHTML:@"<span style='text-decoration: none'> sixth</span>" andExpectEditorStateWith:@{ @"underline": @NO }];
+    [testHarness toggleUnderline];
+    [testHarness insertText:@" seventh" andExpectEditorStateWith:@{ @"underline": @YES }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"underline": @YES }];
+    [testHarness deleteBackwardAndExpectEditorStateWith:@{ @"underline": @YES }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"underline": @YES }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"underline": @NO }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"underline": @YES }];
+    [testHarness moveWordBackwardAndExpectEditorStateWith:@{ @"underline": @NO }];
+
+    [testHarness selectAllAndExpectEditorStateWith:@{ @"underline": @YES }];
+    EXPECT_WK_STREQ("first second third fourth fifth sixth seventh", [[testHarness webView] stringByEvaluatingJavaScript:@"getSelection().toString()"]);
+}
+
+TEST(EditorStateTests, TypingAttributesTextAlignmentAbsoluteAlignmentOptions)
+{
+    auto testHarness = setUpEditorStateTestHarness();
+    TestWKWebView *webView = [testHarness webView];
+
+    [webView stringByEvaluatingJavaScript:@"document.body.style.direction = 'ltr'"];
+
+    [testHarness insertHTML:@"<div style='text-align: right;'>right</div>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+
+    [testHarness insertText:@"justified" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+    [testHarness alignJustifiedAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentJustified) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentJustified) }];
+
+    [testHarness alignCenterAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentCenter) }];
+    [testHarness insertText:@"center" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentCenter) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentCenter) }];
+
+    [testHarness insertHTML:@"<span id='left'>left</span>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentCenter) }];
+    [webView stringByEvaluatingJavaScript:@"getSelection().setBaseAndExtent(left.childNodes[0], 0, left.childNodes[0], 6)"];
+    [testHarness alignLeftAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentLeft) }];
+
+    [testHarness selectAllAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+    EXPECT_WK_STREQ("right\njustified\ncenter\nleft", [webView stringByEvaluatingJavaScript:@"getSelection().toString()"]);
+}
+
+TEST(EditorStateTests, TypingAttributesTextAlignmentStartEnd)
+{
+    auto testHarness = setUpEditorStateTestHarness();
+    TestWKWebView *webView = [testHarness webView];
+
+    [webView stringByEvaluatingJavaScript:@"document.styleSheets[0].insertRule('.start { text-align: start; }')"];
+    [webView stringByEvaluatingJavaScript:@"document.styleSheets[0].insertRule('.end { text-align: end; }')"];
+    [webView stringByEvaluatingJavaScript:@"document.body.style.direction = 'rtl'"];
+
+    [testHarness insertHTML:@"<div class='start'>rtl start</div>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+
+    [testHarness insertHTML:@"<div class='end'>rtl end</div>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentLeft) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentLeft) }];
+
+    [[testHarness webView] stringByEvaluatingJavaScript:@"document.body.style.direction = 'ltr'"];
+    [testHarness insertHTML:@"<div class='start'>ltr start</div>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentLeft) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentLeft) }];
+
+    [testHarness insertHTML:@"<div class='end'>ltr end</div>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+}
+
+TEST(EditorStateTests, TypingAttributesTextAlignmentDirectionalText)
+{
+    auto testHarness = setUpEditorStateTestHarness();
+    [[testHarness webView] stringByEvaluatingJavaScript:@"document.body.setAttribute('dir', 'auto')"];
+
+    [testHarness insertHTML:@"<div>מקור השם עברית</div>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+    [testHarness insertHTML:@"<div dir='ltr'>מקור השם עברית</div>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentLeft) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentLeft) }];
+    [testHarness insertHTML:@"<div dir='rtl'>מקור השם עברית</div>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+
+    [testHarness insertHTML:@"<div dir='auto'>This is English text</div>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentLeft) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentLeft) }];
+    [testHarness insertHTML:@"<div dir='rtl'>This is English text</div>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentRight) }];
+    [testHarness insertHTML:@"<div dir='ltr'>This is English text</div>" andExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentLeft) }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-alignment": @(NSTextAlignmentLeft) }];
+}
+
+TEST(EditorStateTests, TypingAttributesTextColor)
+{
+    auto testHarness = setUpEditorStateTestHarness();
+
+    [testHarness setForegroundColor:@"rgb(255, 0, 0)"];
+    [testHarness insertText:@"red" andExpectEditorStateWith:@{ @"text-color": @"rgb(255, 0, 0)" }];
+
+    [testHarness insertHTML:@"<span style='color: rgb(0, 255, 0)'>green</span>" andExpectEditorStateWith:@{ @"text-color": @"rgb(0, 255, 0)" }];
+    [testHarness insertParagraphAndExpectEditorStateWith:@{ @"text-color": @"rgb(0, 255, 0)" }];
+
+    [testHarness setForegroundColor:@"rgb(0, 0, 255)"];
+    [testHarness insertText:@"blue" andExpectEditorStateWith:@{ @"text-color": @"rgb(0, 0, 255)" }];
+}
+
+TEST(EditorStateTests, TypingAttributesMixedStyles)
+{
+    auto testHarness = setUpEditorStateTestHarness();
+
+    [testHarness setForegroundColor:@"rgb(128, 128, 128)"];
+    [testHarness toggleBold];
+    [testHarness toggleItalic];
+    [testHarness toggleUnderline];
+    [testHarness alignCenterAndExpectEditorStateWith:@{
+        @"bold": @YES,
+        @"italic": @YES,
+        @"underline": @YES,
+        @"text-color": @"rgb(128, 128, 128)",
+        @"text-alignment": @(NSTextAlignmentCenter)
+    }];
+}
+
+TEST(EditorStateTests, TypingAttributeLinkColor)
+{
+    auto testHarness = setUpEditorStateTestHarness();
+    [testHarness insertHTML:@"<a href='https://www.apple.com/'>This is a link</a>" andExpectEditorStateWith:@{ @"text-color": @"rgb(0, 0, 238)" }];
+    [testHarness selectAllAndExpectEditorStateWith:@{ @"text-color": @"rgb(0, 0, 238)" }];
+    EXPECT_WK_STREQ("https://www.apple.com/", [[testHarness webView] stringByEvaluatingJavaScript:@"document.querySelector('a').href"]);
+}
+
+} // namespace TestWebKitAPI
+
+#endif // WK_API_ENABLED
diff --git a/Tools/TestWebKitAPI/Tests/WebKit2Cocoa/editor-state-test-harness.html b/Tools/TestWebKitAPI/Tests/WebKit2Cocoa/editor-state-test-harness.html
new file mode 100644 (file)
index 0000000..3630d93
--- /dev/null
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<style>
+body, html {
+    font-family: -apple-system;
+    font-size: 1em;
+    width: 100%;
+    height: 100%;
+}
+</style>
+<body contenteditable></body>
+<script>
+document.body.focus();
+</script>