[Attachment Support] Implement delegate hooks for attachment element insertion and...
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 6 Nov 2017 21:46:54 +0000 (21:46 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 6 Nov 2017 21:46:54 +0000 (21:46 +0000)
https://bugs.webkit.org/show_bug.cgi?id=179016
<rdar://problem/35250890>

Reviewed by Tim Horton.

Source/WebCore:

Implements a mechanism for notifying WebKit2 clients when attachment elements are inserted into or removed from
the document. See per-change comments below for more details.

API tests: WKAttachmentTests.AttachmentElementInsertion
           WKAttachmentTests.AttachmentUpdatesWhenInsertingAndDeletingNewline
           WKAttachmentTests.AttachmentUpdatesWhenUndoingAndRedoing
           WKAttachmentTests.AttachmentUpdatesWhenChangingFontStyles
           WKAttachmentTests.AttachmentUpdatesWhenInsertingLists
           WKAttachmentTests.AttachmentUpdatesWhenInsertingRichMarkup

* editing/Editor.cpp:
(WebCore::Editor::respondToChangedSelection):
(WebCore::Editor::editorUIUpdateTimerFired):

Additionally notify the client of any attachment updates.

(WebCore::Editor::scheduleEditorUIUpdate):

Add a new helper that starts the editor UI update timer with 0 delay, and use it everywhere we schedule an
editor UI update.

(WebCore::Editor::didInsertAttachmentElement):
(WebCore::Editor::didRemoveAttachmentElement):

Maintain two sets of attachment element identifiers -- the first one tracking insertions, and the second one
tracking removals. When an attachment element is inserted, we first check to see if that attachment element has
just been removed; if so, we don't add it to the inserted identifiers set, but instead remove it from the set of
removed identifiers. We perform a similar check in the opposite case. This prevents us from notifying the client
of extraneous insertions and removals during certain editing commands which may reparent and move attachment
elements around. In both cases, we schedule an editor UI update afterwards, where we will notify the client of
attachment updates.

(WebCore::Editor::notifyClientOfAttachmentUpdates):
(WebCore::Editor::insertAttachmentFromFile):
* editing/Editor.h:
* html/HTMLAttachmentElement.cpp:
(WebCore::HTMLAttachmentElement::HTMLAttachmentElement):

Remove the version of HTMLAttachmentElement's constructor that takes a unique identifier.

(WebCore::HTMLAttachmentElement::insertedIntoAncestor):
(WebCore::HTMLAttachmentElement::removedFromAncestor):

Implement these hooks to observe insertion into and removal from the DOM. If the element was attached to or
removed from an ancestor that was connected to the document, call out to the document's frame's editor. This
"document-connected" rule prevents us from calling out to the client in cases where (for instance) we append an
attachment element to a newly created DocumentFragment in preparation for executing a ReplaceSelectionCommand.

(WebCore::HTMLAttachmentElement::uniqueIdentifier const):
(WebCore::HTMLAttachmentElement::setUniqueIdentifier):

Refactor unique identifier to refer to the new attachment identifier attribute instead of a member variable.

* html/HTMLAttachmentElement.h:
* html/HTMLAttributeNames.in:

Add a new attribute representing an attachment element's identifier. This enables us to keep track of particular
attachments as they are destroyed and recreated as different objects, as a result of some editing commands.

* page/EditorClient.h:
(WebCore::EditorClient::didInsertAttachment):
(WebCore::EditorClient::didRemoveAttachment):

Add boilerplate editor client hooks for attachment insertion and removal.

Source/WebKit:

Adds boilerplate plumbing to WebEditorClient, WebPage, and the usual machinery in the UI process to notify
WebKit2 clients when attachment elements have been inserted or removed from the document. See the WebCore
ChangeLog for more details about the implementation, or the Tools ChangeLog for more information about new API
tests.

* UIProcess/API/Cocoa/WKUIDelegatePrivate.h:
* UIProcess/API/Cocoa/WKWebView.mm:
(-[WKWebView _didInsertAttachment:]):
(-[WKWebView _didRemoveAttachment:]):
* UIProcess/API/Cocoa/WKWebViewInternal.h:
* UIProcess/Cocoa/PageClientImplCocoa.h:
* UIProcess/Cocoa/PageClientImplCocoa.mm:
(WebKit::PageClientImplCocoa::didInsertAttachment):
(WebKit::PageClientImplCocoa::didRemoveAttachment):
* UIProcess/PageClient.h:
(WebKit::PageClient::didInsertAttachment):
(WebKit::PageClient::didRemoveAttachment):
* UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::didInsertAttachment):
(WebKit::WebPageProxy::didRemoveAttachment):
* UIProcess/WebPageProxy.h:
* UIProcess/WebPageProxy.messages.in:
* WebProcess/WebCoreSupport/WebEditorClient.cpp:
(WebKit::WebEditorClient::didInsertAttachment):
(WebKit::WebEditorClient::didRemoveAttachment):
* WebProcess/WebCoreSupport/WebEditorClient.h:

Tools:

Introduces new API tests to check that various editing operations will or won't result in the new attachment
insertion and removal delegate hooks being fired. Additionally refactors an existing test to verify that
attachments insertion and removal is observable by the UI delegate.

* TestWebKitAPI/Tests/WebKitCocoa/WKAttachmentTests.mm:
(-[AttachmentUpdateObserver init]):
(-[AttachmentUpdateObserver inserted]):
(-[AttachmentUpdateObserver removed]):
(-[AttachmentUpdateObserver _webView:didInsertAttachment:]):
(-[AttachmentUpdateObserver _webView:didRemoveAttachment:]):
(TestWebKitAPI::ObserveAttachmentUpdatesForScope::ObserveAttachmentUpdatesForScope):
(TestWebKitAPI::ObserveAttachmentUpdatesForScope::~ObserveAttachmentUpdatesForScope):
(TestWebKitAPI::ObserveAttachmentUpdatesForScope::expectAttachmentUpdates):

Implement a testing mechanism to temporarily bind a UI delegate to a given WKWebView and listen for inserted or
removed attachments over the course of a particular scope. The API tests use this mechanism to check that the UI
delegate hooks added in this patch are invoked with the right attachments when performing edit commands.

(-[TestWKWebView _synchronouslyExecuteEditCommand:argument:]):
(-[TestWKWebView expectUpdatesAfterCommand:withArgument:expectedRemovals:expectedInsertions:]):
(TestWebKitAPI::TEST):

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

21 files changed:
Source/WebCore/ChangeLog
Source/WebCore/editing/Editor.cpp
Source/WebCore/editing/Editor.h
Source/WebCore/html/HTMLAttachmentElement.cpp
Source/WebCore/html/HTMLAttachmentElement.h
Source/WebCore/html/HTMLAttributeNames.in
Source/WebCore/page/EditorClient.h
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/Cocoa/PageClientImplCocoa.h
Source/WebKit/UIProcess/Cocoa/PageClientImplCocoa.mm
Source/WebKit/UIProcess/PageClient.h
Source/WebKit/UIProcess/WebPageProxy.cpp
Source/WebKit/UIProcess/WebPageProxy.h
Source/WebKit/UIProcess/WebPageProxy.messages.in
Source/WebKit/WebProcess/WebCoreSupport/WebEditorClient.cpp
Source/WebKit/WebProcess/WebCoreSupport/WebEditorClient.h
Tools/ChangeLog
Tools/TestWebKitAPI/Tests/WebKitCocoa/WKAttachmentTests.mm

index a7171ec..e743fa1 100644 (file)
@@ -1,3 +1,76 @@
+2017-11-06  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Attachment Support] Implement delegate hooks for attachment element insertion and removal
+        https://bugs.webkit.org/show_bug.cgi?id=179016
+        <rdar://problem/35250890>
+
+        Reviewed by Tim Horton.
+
+        Implements a mechanism for notifying WebKit2 clients when attachment elements are inserted into or removed from
+        the document. See per-change comments below for more details.
+
+        API tests: WKAttachmentTests.AttachmentElementInsertion
+                   WKAttachmentTests.AttachmentUpdatesWhenInsertingAndDeletingNewline
+                   WKAttachmentTests.AttachmentUpdatesWhenUndoingAndRedoing
+                   WKAttachmentTests.AttachmentUpdatesWhenChangingFontStyles
+                   WKAttachmentTests.AttachmentUpdatesWhenInsertingLists
+                   WKAttachmentTests.AttachmentUpdatesWhenInsertingRichMarkup
+
+        * editing/Editor.cpp:
+        (WebCore::Editor::respondToChangedSelection):
+        (WebCore::Editor::editorUIUpdateTimerFired):
+
+        Additionally notify the client of any attachment updates.
+
+        (WebCore::Editor::scheduleEditorUIUpdate):
+
+        Add a new helper that starts the editor UI update timer with 0 delay, and use it everywhere we schedule an
+        editor UI update.
+
+        (WebCore::Editor::didInsertAttachmentElement):
+        (WebCore::Editor::didRemoveAttachmentElement):
+
+        Maintain two sets of attachment element identifiers -- the first one tracking insertions, and the second one
+        tracking removals. When an attachment element is inserted, we first check to see if that attachment element has
+        just been removed; if so, we don't add it to the inserted identifiers set, but instead remove it from the set of
+        removed identifiers. We perform a similar check in the opposite case. This prevents us from notifying the client
+        of extraneous insertions and removals during certain editing commands which may reparent and move attachment
+        elements around. In both cases, we schedule an editor UI update afterwards, where we will notify the client of
+        attachment updates.
+
+        (WebCore::Editor::notifyClientOfAttachmentUpdates):
+        (WebCore::Editor::insertAttachmentFromFile):
+        * editing/Editor.h:
+        * html/HTMLAttachmentElement.cpp:
+        (WebCore::HTMLAttachmentElement::HTMLAttachmentElement):
+
+        Remove the version of HTMLAttachmentElement's constructor that takes a unique identifier.
+
+        (WebCore::HTMLAttachmentElement::insertedIntoAncestor):
+        (WebCore::HTMLAttachmentElement::removedFromAncestor):
+
+        Implement these hooks to observe insertion into and removal from the DOM. If the element was attached to or
+        removed from an ancestor that was connected to the document, call out to the document's frame's editor. This
+        "document-connected" rule prevents us from calling out to the client in cases where (for instance) we append an
+        attachment element to a newly created DocumentFragment in preparation for executing a ReplaceSelectionCommand.
+
+        (WebCore::HTMLAttachmentElement::uniqueIdentifier const):
+        (WebCore::HTMLAttachmentElement::setUniqueIdentifier):
+
+        Refactor unique identifier to refer to the new attachment identifier attribute instead of a member variable.
+
+        * html/HTMLAttachmentElement.h:
+        * html/HTMLAttributeNames.in:
+
+        Add a new attribute representing an attachment element's identifier. This enables us to keep track of particular
+        attachments as they are destroyed and recreated as different objects, as a result of some editing commands.
+
+        * page/EditorClient.h:
+        (WebCore::EditorClient::didInsertAttachment):
+        (WebCore::EditorClient::didRemoveAttachment):
+
+        Add boilerplate editor client hooks for attachment insertion and removal.
+
 2017-11-06  Ryan Haddad  <ryanhaddad@apple.com>
 
         Unreviewed, rolling out r224494.
index 3a60853..95ba418 100644 (file)
@@ -3428,7 +3428,7 @@ void Editor::respondToChangedSelection(const VisibleSelection&, FrameSelection::
     m_editorUIUpdateTimerShouldCheckSpellingAndGrammar = options & FrameSelection::CloseTyping
         && !(options & FrameSelection::SpellCorrectionTriggered);
     m_editorUIUpdateTimerWasTriggeredByDictation = options & FrameSelection::DictationTriggered;
-    m_editorUIUpdateTimer.startOneShot(0_s);
+    scheduleEditorUIUpdate();
 }
 
 #if ENABLE(TELEPHONE_NUMBER_DETECTION) && !PLATFORM(IOS)
@@ -3601,6 +3601,10 @@ void Editor::editorUIUpdateTimerFired()
         m_alternativeTextController->respondToChangedSelection(oldSelection);
 
     m_oldSelectionForEditorUIUpdate = m_frame.selection().selection();
+
+#if ENABLE(ATTACHMENT_ELEMENT)
+    notifyClientOfAttachmentUpdates();
+#endif
 }
 
 static Node* findFirstMarkable(Node* node)
@@ -3733,8 +3737,48 @@ RefPtr<Range> Editor::rangeForTextCheckingResult(const TextCheckingResult& resul
     return TextIterator::subrange(*contextRange, result.location, result.length);
 }
 
+void Editor::scheduleEditorUIUpdate()
+{
+    m_editorUIUpdateTimer.startOneShot(0_s);
+}
+
 #if ENABLE(ATTACHMENT_ELEMENT)
 
+void Editor::didInsertAttachmentElement(HTMLAttachmentElement& attachment)
+{
+    auto identifier = attachment.uniqueIdentifier();
+    if (identifier.isEmpty())
+        return;
+
+    if (!m_removedAttachmentIdentifiers.take(identifier))
+        m_insertedAttachmentIdentifiers.add(identifier);
+    scheduleEditorUIUpdate();
+}
+
+void Editor::didRemoveAttachmentElement(HTMLAttachmentElement& attachment)
+{
+    auto identifier = attachment.uniqueIdentifier();
+    if (identifier.isEmpty())
+        return;
+
+    if (!m_insertedAttachmentIdentifiers.take(identifier))
+        m_removedAttachmentIdentifiers.add(identifier);
+    scheduleEditorUIUpdate();
+}
+
+void Editor::notifyClientOfAttachmentUpdates()
+{
+    if (auto* editorClient = client()) {
+        for (auto& identifier : m_removedAttachmentIdentifiers)
+            editorClient->didRemoveAttachment(identifier);
+        for (auto& identifier : m_insertedAttachmentIdentifiers)
+            editorClient->didInsertAttachment(identifier);
+    }
+
+    m_removedAttachmentIdentifiers.clear();
+    m_insertedAttachmentIdentifiers.clear();
+}
+
 void Editor::insertAttachment(const String& identifier, const String& filename, const String& filepath, std::optional<String> contentType)
 {
     if (!contentType)
@@ -3751,7 +3795,7 @@ void Editor::insertAttachment(const String& identifier, const String& filename,
 
 void Editor::insertAttachmentFromFile(const String& identifier, const String& filename, const String& contentType, Ref<File>&& file)
 {
-    auto attachment = HTMLAttachmentElement::create(HTMLNames::attachmentTag, document(), identifier);
+    auto attachment = HTMLAttachmentElement::create(HTMLNames::attachmentTag, document());
     attachment->setAttribute(HTMLNames::titleAttr, filename);
     attachment->setAttribute(HTMLNames::subtitleAttr, fileSizeDescription(file->size()));
     attachment->setAttribute(HTMLNames::typeAttr, contentType);
index 2ba89b6..ea326a8 100644 (file)
@@ -503,6 +503,8 @@ public:
 #if ENABLE(ATTACHMENT_ELEMENT)
     WEBCORE_EXPORT void insertAttachment(const String& identifier, const String& filename, const String& filepath, std::optional<String> contentType = std::nullopt);
     WEBCORE_EXPORT void insertAttachment(const String& identifier, const String& filename, Ref<SharedBuffer>&& data, std::optional<String> contentType = std::nullopt);
+    void didInsertAttachmentElement(HTMLAttachmentElement&);
+    void didRemoveAttachmentElement(HTMLAttachmentElement&);
 #endif
 
 private:
@@ -550,6 +552,12 @@ private:
     static RefPtr<SharedBuffer> dataInRTFFormat(NSAttributedString *);
 #endif
 
+    void scheduleEditorUIUpdate();
+
+#if ENABLE(ATTACHMENT_ELEMENT)
+    void notifyClientOfAttachmentUpdates();
+#endif
+
     void postTextStateChangeNotificationForCut(const String&, const VisibleSelection&);
 
     Frame& m_frame;
@@ -569,6 +577,11 @@ private:
     EditorParagraphSeparator m_defaultParagraphSeparator { EditorParagraphSeparatorIsDiv };
     bool m_overwriteModeEnabled { false };
 
+#if ENABLE(ATTACHMENT_ELEMENT)
+    HashSet<String> m_insertedAttachmentIdentifiers;
+    HashSet<String> m_removedAttachmentIdentifiers;
+#endif
+
     VisibleSelection m_oldSelectionForEditorUIUpdate;
     Timer m_editorUIUpdateTimer;
     bool m_editorUIUpdateTimerShouldCheckSpellingAndGrammar { false };
index 3bff0c7..d136b48 100644 (file)
 #include "Frame.h"
 #include "HTMLNames.h"
 #include "RenderAttachment.h"
-#include <wtf/UUID.h>
 
 namespace WebCore {
 
 using namespace HTMLNames;
 
-HTMLAttachmentElement::HTMLAttachmentElement(const QualifiedName& tagName, Document& document, const String& identifier)
+HTMLAttachmentElement::HTMLAttachmentElement(const QualifiedName& tagName, Document& document)
     : HTMLElement(tagName, document)
-    , m_uniqueIdentifier(identifier)
 {
     ASSERT(hasTagName(attachmentTag));
 }
 
-HTMLAttachmentElement::HTMLAttachmentElement(const QualifiedName& tagName, Document& document)
-    : HTMLAttachmentElement(tagName, document, createCanonicalUUIDString())
-{
-}
-
 HTMLAttachmentElement::~HTMLAttachmentElement() = default;
 
 Ref<HTMLAttachmentElement> HTMLAttachmentElement::create(const QualifiedName& tagName, Document& document)
@@ -58,11 +51,6 @@ Ref<HTMLAttachmentElement> HTMLAttachmentElement::create(const QualifiedName& ta
     return adoptRef(*new HTMLAttachmentElement(tagName, document));
 }
 
-Ref<HTMLAttachmentElement> HTMLAttachmentElement::create(const QualifiedName& tagName, Document& document, const String& identifier)
-{
-    return adoptRef(*new HTMLAttachmentElement(tagName, document, identifier));
-}
-
 RenderPtr<RenderElement> HTMLAttachmentElement::createElementRenderer(RenderStyle&& style, const RenderTreePosition&)
 {
     return createRenderer<RenderAttachment>(*this, WTFMove(style));
@@ -81,6 +69,25 @@ void HTMLAttachmentElement::setFile(File* file)
         renderer->invalidate();
 }
 
+Node::InsertedIntoAncestorResult HTMLAttachmentElement::insertedIntoAncestor(InsertionType type, ContainerNode& ancestor)
+{
+    auto result = HTMLElement::insertedIntoAncestor(type, ancestor);
+    if (auto* frame = document().frame()) {
+        if (type.connectedToDocument)
+            frame->editor().didInsertAttachmentElement(*this);
+    }
+    return result;
+}
+
+void HTMLAttachmentElement::removedFromAncestor(RemovalType type, ContainerNode& ancestor)
+{
+    HTMLElement::removedFromAncestor(type, ancestor);
+    if (auto* frame = document().frame()) {
+        if (type.disconnectedFromDocument)
+            frame->editor().didRemoveAttachmentElement(*this);
+    }
+}
+
 void HTMLAttachmentElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
 {
     if (name == progressAttr || name == subtitleAttr || name == titleAttr || name == typeAttr) {
@@ -91,6 +98,16 @@ void HTMLAttachmentElement::parseAttribute(const QualifiedName& name, const Atom
     HTMLElement::parseAttribute(name, value);
 }
 
+String HTMLAttachmentElement::uniqueIdentifier() const
+{
+    return attributeWithoutSynchronization(HTMLNames::webkitattachmentidAttr);
+}
+
+void HTMLAttachmentElement::setUniqueIdentifier(const String& identifier)
+{
+    setAttributeWithoutSynchronization(HTMLNames::webkitattachmentidAttr, identifier);
+}
+
 String HTMLAttachmentElement::attachmentTitle() const
 {
     auto& title = attributeWithoutSynchronization(titleAttr);
index 3a44a5f..bd9c564 100644 (file)
@@ -37,13 +37,15 @@ class RenderAttachment;
 class HTMLAttachmentElement final : public HTMLElement {
 public:
     static Ref<HTMLAttachmentElement> create(const QualifiedName&, Document&);
-    static Ref<HTMLAttachmentElement> create(const QualifiedName&, Document&, const String& identifier);
 
     WEBCORE_EXPORT File* file() const;
     void setFile(File*);
 
-    WEBCORE_EXPORT String uniqueIdentifier() const { return m_uniqueIdentifier; }
-    void setUniqueIdentifier(const String& identifier) { m_uniqueIdentifier = identifier; }
+    WEBCORE_EXPORT String uniqueIdentifier() const;
+    void setUniqueIdentifier(const String&);
+
+    InsertedIntoAncestorResult insertedIntoAncestor(InsertionType, ContainerNode&) final;
+    void removedFromAncestor(RemovalType, ContainerNode&) final;
 
     WEBCORE_EXPORT String attachmentTitle() const;
     String attachmentType() const;
@@ -52,7 +54,6 @@ public:
 
 private:
     HTMLAttachmentElement(const QualifiedName&, Document&);
-    HTMLAttachmentElement(const QualifiedName&, Document&, const String& identifier);
     virtual ~HTMLAttachmentElement();
 
     RenderPtr<RenderElement> createElementRenderer(RenderStyle&&, const RenderTreePosition&) final;
@@ -68,7 +69,6 @@ private:
     void parseAttribute(const QualifiedName&, const AtomicString&) final;
     
     RefPtr<File> m_file;
-    String m_uniqueIdentifier;
 };
 
 } // namespace WebCore
index 97b6742..b6f0433 100644 (file)
@@ -381,6 +381,7 @@ version
 vlink
 vspace
 webkitallowfullscreen
+webkitattachmentid
 webkitattachmentpath
 webkitdirectory
 width
index 30c09d2..e02cc09 100644 (file)
@@ -72,6 +72,11 @@ public:
     virtual void didApplyStyle() = 0;
     virtual bool shouldMoveRangeAfterDelete(Range*, Range*) = 0;
 
+#if ENABLE(ATTACHMENT_ELEMENT)
+    virtual void didInsertAttachment(const String&) { }
+    virtual void didRemoveAttachment(const String&) { }
+#endif
+
     virtual void didBeginEditing() = 0;
     virtual void respondToChangedContents() = 0;
     virtual void respondToChangedSelection(Frame*) = 0;
index d78c70e..06e4451 100644 (file)
@@ -1,3 +1,38 @@
+2017-11-06  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Attachment Support] Implement delegate hooks for attachment element insertion and removal
+        https://bugs.webkit.org/show_bug.cgi?id=179016
+        <rdar://problem/35250890>
+
+        Reviewed by Tim Horton.
+
+        Adds boilerplate plumbing to WebEditorClient, WebPage, and the usual machinery in the UI process to notify
+        WebKit2 clients when attachment elements have been inserted or removed from the document. See the WebCore
+        ChangeLog for more details about the implementation, or the Tools ChangeLog for more information about new API
+        tests.
+
+        * UIProcess/API/Cocoa/WKUIDelegatePrivate.h:
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (-[WKWebView _didInsertAttachment:]):
+        (-[WKWebView _didRemoveAttachment:]):
+        * UIProcess/API/Cocoa/WKWebViewInternal.h:
+        * UIProcess/Cocoa/PageClientImplCocoa.h:
+        * UIProcess/Cocoa/PageClientImplCocoa.mm:
+        (WebKit::PageClientImplCocoa::didInsertAttachment):
+        (WebKit::PageClientImplCocoa::didRemoveAttachment):
+        * UIProcess/PageClient.h:
+        (WebKit::PageClient::didInsertAttachment):
+        (WebKit::PageClient::didRemoveAttachment):
+        * UIProcess/WebPageProxy.cpp:
+        (WebKit::WebPageProxy::didInsertAttachment):
+        (WebKit::WebPageProxy::didRemoveAttachment):
+        * UIProcess/WebPageProxy.h:
+        * UIProcess/WebPageProxy.messages.in:
+        * WebProcess/WebCoreSupport/WebEditorClient.cpp:
+        (WebKit::WebEditorClient::didInsertAttachment):
+        (WebKit::WebEditorClient::didRemoveAttachment):
+        * WebProcess/WebCoreSupport/WebEditorClient.h:
+
 2017-11-06  Jeremy Jones  <jeremyj@apple.com>
 
         iOS element fullscreen should use a UIGestureRecognizer to detect user interaction.
index c087b89..3b2e8cb 100644 (file)
@@ -110,6 +110,9 @@ struct UIEdgeInsets;
 - (void)_webView:(WKWebView *)webView runBeforeUnloadConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler WK_API_AVAILABLE(macosx(10.13), ios(11.0));
 - (void)_webView:(WKWebView *)webView editorStateDidChange:(NSDictionary *)editorState WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));
 
+- (void)_webView:(WKWebView *)webView didRemoveAttachment:(_WKAttachment *)attachment WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));
+- (void)_webView:(WKWebView *)webView didInsertAttachment:(_WKAttachment *)attachment 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));
 - (NSArray *)_webView:(WKWebView *)webView actionsForElement:(_WKActivatedElementInfo *)element defaultActions:(NSArray<_WKElementAction *> *)defaultActions;
index b185014..2c6b5d4 100644 (file)
@@ -1197,6 +1197,24 @@ static NSDictionary *dictionaryRepresentationForEditorState(const WebKit::Editor
         [uiDelegate _webView:self editorStateDidChange:dictionaryRepresentationForEditorState(_page->editorState())];
 }
 
+#if ENABLE(ATTACHMENT_ELEMENT)
+
+- (void)_didInsertAttachment:(NSString *)identifier
+{
+    id <WKUIDelegatePrivate> uiDelegate = (id <WKUIDelegatePrivate>)self.UIDelegate;
+    if ([uiDelegate respondsToSelector:@selector(_webView:didInsertAttachment:)])
+        [uiDelegate _webView:self didInsertAttachment:[wrapper(API::Attachment::create(identifier, *_page).leakRef()) autorelease]];
+}
+
+- (void)_didRemoveAttachment:(NSString *)identifier
+{
+    id <WKUIDelegatePrivate> uiDelegate = (id <WKUIDelegatePrivate>)self.UIDelegate;
+    if ([uiDelegate respondsToSelector:@selector(_webView:didRemoveAttachment:)])
+        [uiDelegate _webView:self didRemoveAttachment:[wrapper(API::Attachment::create(identifier, *_page).leakRef()) autorelease]];
+}
+
+#endif // ENABLE(ATTACHMENT_ELEMENT)
+
 #pragma mark iOS-specific methods
 
 #if PLATFORM(IOS)
index 48de037..8097e04 100644 (file)
@@ -148,6 +148,11 @@ struct PrintInfo;
 @property (nonatomic, readonly) UIEdgeInsets _computedUnobscuredSafeAreaInset;
 #endif
 
+#if ENABLE(ATTACHMENT_ELEMENT)
+- (void)_didRemoveAttachment:(NSString *)identifier;
+- (void)_didInsertAttachment:(NSString *)identifier;
+#endif
+
 - (WKPageRef)_pageForTesting;
 - (WebKit::WebPageProxy*)_page;
 
index d8cf613..ffc2343 100644 (file)
@@ -37,6 +37,12 @@ public:
         : m_webView(webView) { }
     void isPlayingAudioWillChange() final;
     void isPlayingAudioDidChange() final;
+
+#if ENABLE(ATTACHMENT_ELEMENT)
+    void didInsertAttachment(const String& identifier) final;
+    void didRemoveAttachment(const String& identifier) final;
+#endif
+
 protected:
     WKWebView *m_webView;
 };
index 1ac2812..2d862c4 100644 (file)
@@ -26,7 +26,7 @@
 #import "config.h"
 #import "PageClientImplCocoa.h"
 
-#import "WKWebViewPrivate.h"
+#import "WKWebViewInternal.h"
 
 namespace WebKit {
 
@@ -43,5 +43,27 @@ void PageClientImplCocoa::isPlayingAudioDidChange()
     [m_webView didChangeValueForKey:NSStringFromSelector(@selector(_isPlayingAudio))];
 #endif
 }
+
+#if ENABLE(ATTACHMENT_ELEMENT)
+
+void PageClientImplCocoa::didInsertAttachment(const String& identifier)
+{
+#if WK_API_ENABLED
+    [m_webView _didInsertAttachment:identifier];
+#else
+    UNUSED_PARAM(identifier);
+#endif
+}
+
+void PageClientImplCocoa::didRemoveAttachment(const String& identifier)
+{
+#if WK_API_ENABLED
+    [m_webView _didRemoveAttachment:identifier];
+#else
+    UNUSED_PARAM(identifier);
+#endif
+}
+
+#endif
     
 }
index 04710fd..5005ba9 100644 (file)
@@ -384,6 +384,11 @@ public:
     virtual void didChangeDataInteractionCaretRect(const WebCore::IntRect& previousCaretRect, const WebCore::IntRect& caretRect) = 0;
 #endif
 
+#if ENABLE(ATTACHMENT_ELEMENT)
+    virtual void didInsertAttachment(const String& identifier) { }
+    virtual void didRemoveAttachment(const String& identifier) { }
+#endif
+
 #if PLATFORM(GTK) || PLATFORM(WPE)
     virtual JSGlobalContextRef javascriptGlobalContext() { return nullptr; }
 #endif
index eef13b3..1867757 100644 (file)
@@ -7113,6 +7113,16 @@ void WebPageProxy::insertAttachment(const String& identifier, const String& file
     m_process->send(Messages::WebPage::InsertAttachment(identifier, filename, contentType, IPC::SharedBufferDataReference { &data }, callbackID), m_pageID);
 }
 
+void WebPageProxy::didInsertAttachment(const String& identifier)
+{
+    m_pageClient.didInsertAttachment(identifier);
+}
+
+void WebPageProxy::didRemoveAttachment(const String& identifier)
+{
+    m_pageClient.didRemoveAttachment(identifier);
+}
+
 #endif // ENABLE(ATTACHMENT_ELEMENT)
 
 } // namespace WebKit
index 1067eb9..3e68236 100644 (file)
@@ -1637,6 +1637,11 @@ private:
 
     void stopAllURLSchemeTasks();
 
+#if ENABLE(ATTACHMENT_ELEMENT)
+    void didInsertAttachment(const String& identifier);
+    void didRemoveAttachment(const String& identifier);
+#endif
+
     PageClient& m_pageClient;
     Ref<API::PageConfiguration> m_configuration;
 
index 5be914b..f1ef5e5 100644 (file)
@@ -498,4 +498,9 @@ messages -> WebPageProxy {
     StopURLSchemeTask(uint64_t handlerIdentifier, uint64_t taskIdentifier)
 
     RequestStorageAccess(String subFrameHost, String topFrameHost, uint64_t contextID)
+
+#if ENABLE(ATTACHMENT_ELEMENT)
+    DidInsertAttachment(String identifier)
+    DidRemoveAttachment(String identifier)
+#endif
 }
index 00fa7a3..5393df2 100644 (file)
@@ -157,6 +157,20 @@ bool WebEditorClient::shouldApplyStyle(StyleProperties* style, Range* range)
     return result;
 }
 
+#if ENABLE(ATTACHMENT_ELEMENT)
+
+void WebEditorClient::didInsertAttachment(const String& identifier)
+{
+    m_page->send(Messages::WebPageProxy::DidInsertAttachment(identifier));
+}
+
+void WebEditorClient::didRemoveAttachment(const String& identifier)
+{
+    m_page->send(Messages::WebPageProxy::DidRemoveAttachment(identifier));
+}
+
+#endif
+
 void WebEditorClient::didApplyStyle()
 {
     m_page->didApplyStyle();
index 4154f9a..a8a61b0 100644 (file)
@@ -59,6 +59,11 @@ private:
     void didApplyStyle() final;
     bool shouldMoveRangeAfterDelete(WebCore::Range*, WebCore::Range*) final;
 
+#if ENABLE(ATTACHMENT_ELEMENT)
+    void didInsertAttachment(const String& identifier) final;
+    void didRemoveAttachment(const String& identifier) final;
+#endif
+
     void didBeginEditing() final;
     void respondToChangedContents() final;
     void respondToChangedSelection(WebCore::Frame*) final;
index 2a237ec..b2ef274 100644 (file)
@@ -1,3 +1,33 @@
+2017-11-06  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Attachment Support] Implement delegate hooks for attachment element insertion and removal
+        https://bugs.webkit.org/show_bug.cgi?id=179016
+        <rdar://problem/35250890>
+
+        Reviewed by Tim Horton.
+
+        Introduces new API tests to check that various editing operations will or won't result in the new attachment
+        insertion and removal delegate hooks being fired. Additionally refactors an existing test to verify that
+        attachments insertion and removal is observable by the UI delegate.
+
+        * TestWebKitAPI/Tests/WebKitCocoa/WKAttachmentTests.mm:
+        (-[AttachmentUpdateObserver init]):
+        (-[AttachmentUpdateObserver inserted]):
+        (-[AttachmentUpdateObserver removed]):
+        (-[AttachmentUpdateObserver _webView:didInsertAttachment:]):
+        (-[AttachmentUpdateObserver _webView:didRemoveAttachment:]):
+        (TestWebKitAPI::ObserveAttachmentUpdatesForScope::ObserveAttachmentUpdatesForScope):
+        (TestWebKitAPI::ObserveAttachmentUpdatesForScope::~ObserveAttachmentUpdatesForScope):
+        (TestWebKitAPI::ObserveAttachmentUpdatesForScope::expectAttachmentUpdates):
+
+        Implement a testing mechanism to temporarily bind a UI delegate to a given WKWebView and listen for inserted or
+        removed attachments over the course of a particular scope. The API tests use this mechanism to check that the UI
+        delegate hooks added in this patch are invoked with the right attachments when performing edit commands.
+
+        (-[TestWKWebView _synchronouslyExecuteEditCommand:argument:]):
+        (-[TestWKWebView expectUpdatesAfterCommand:withArgument:expectedRemovals:expectedInsertions:]):
+        (TestWebKitAPI::TEST):
+
 2017-11-06  Christopher Reid  <chris.reid@sony.com>
 
         Use enum classes within FileSystem
index 2ec23f4..0708137 100644 (file)
 
 #if WK_API_ENABLED
 
+@interface AttachmentUpdateObserver : NSObject <WKUIDelegatePrivate>
+@property (nonatomic, readonly) NSArray *inserted;
+@property (nonatomic, readonly) NSArray *removed;
+@end
+
+@implementation AttachmentUpdateObserver {
+    RetainPtr<NSMutableArray<_WKAttachment *>> _inserted;
+    RetainPtr<NSMutableArray<_WKAttachment *>> _removed;
+}
+
+- (instancetype)init
+{
+    if (self = [super init]) {
+        _inserted = adoptNS([[NSMutableArray alloc] init]);
+        _removed = adoptNS([[NSMutableArray alloc] init]);
+    }
+    return self;
+}
+
+- (NSArray<_WKAttachment *> *)inserted
+{
+    return _inserted.get();
+}
+
+- (NSArray<_WKAttachment *> *)removed
+{
+    return _removed.get();
+}
+
+- (void)_webView:(WKWebView *)webView didInsertAttachment:(_WKAttachment *)attachment
+{
+    [_inserted addObject:attachment];
+}
+
+- (void)_webView:(WKWebView *)webView didRemoveAttachment:(_WKAttachment *)attachment
+{
+    [_removed addObject:attachment];
+}
+
+@end
+
+namespace TestWebKitAPI {
+
+class ObserveAttachmentUpdatesForScope {
+public:
+    ObserveAttachmentUpdatesForScope(TestWKWebView *webView)
+        : m_webView(webView)
+    {
+        m_previousDelegate = retainPtr(webView.UIDelegate);
+        m_observer = adoptNS([[AttachmentUpdateObserver alloc] init]);
+        webView.UIDelegate = m_observer.get();
+    }
+
+    ~ObserveAttachmentUpdatesForScope()
+    {
+        [m_webView setUIDelegate:m_previousDelegate.get()];
+    }
+
+    AttachmentUpdateObserver *observer() const { return m_observer.get(); }
+
+    void expectAttachmentUpdates(NSArray<_WKAttachment *> *removed, NSArray<_WKAttachment *> *inserted)
+    {
+        BOOL removedAttachmentsMatch = [observer().removed isEqual:removed];
+        if (!removedAttachmentsMatch)
+            NSLog(@"Expected removed attachments: %@ to match %@.", observer().removed, removed);
+        EXPECT_TRUE(removedAttachmentsMatch);
+
+        BOOL insertedAttachmentsMatch = [observer().inserted isEqual:inserted];
+        if (!insertedAttachmentsMatch)
+            NSLog(@"Expected inserted attachments: %@ to match %@.", observer().inserted, inserted);
+        EXPECT_TRUE(insertedAttachmentsMatch);
+    }
+
+private:
+    RetainPtr<AttachmentUpdateObserver> m_observer;
+    RetainPtr<TestWKWebView> m_webView;
+    RetainPtr<id> m_previousDelegate;
+};
+
+}
+
+@interface TestWKWebView (AttachmentTesting)
+@end
+
 static RetainPtr<TestWKWebView> webViewForTestingAttachments()
 {
     auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
@@ -57,6 +141,18 @@ static NSData *testImageData()
 
 @implementation TestWKWebView (AttachmentTesting)
 
+- (BOOL)_synchronouslyExecuteEditCommand:(NSString *)command argument:(NSString *)argument
+{
+    __block bool done = false;
+    __block bool success;
+    [self _executeEditCommand:command argument:argument completion:^(BOOL completionSuccess) {
+        done = true;
+        success = completionSuccess;
+    }];
+    TestWebKitAPI::Util::run(&done);
+    return success;
+}
+
 - (_WKAttachment *)synchronouslyInsertAttachmentWithFilename:(NSString *)filename contentType:(NSString *)contentType data:(NSData *)data options:(_WKAttachmentDisplayOptions *)options
 {
     __block bool done = false;
@@ -72,6 +168,13 @@ static NSData *testImageData()
     return [self stringByEvaluatingJavaScript:[NSString stringWithFormat:@"document.querySelector('%@').getAttribute('%@')", querySelector, attributeName]];
 }
 
+- (void)expectUpdatesAfterCommand:(NSString *)command withArgument:(NSString *)argument expectedRemovals:(NSArray<_WKAttachment *> *)removed expectedInsertions:(NSArray<_WKAttachment *> *)inserted
+{
+    TestWebKitAPI::ObserveAttachmentUpdatesForScope observer(self);
+    EXPECT_TRUE([self _synchronouslyExecuteEditCommand:command argument:argument]);
+    observer.expectAttachmentUpdates(removed, inserted);
+}
+
 @end
 
 namespace TestWebKitAPI {
@@ -79,19 +182,114 @@ namespace TestWebKitAPI {
 TEST(WKAttachmentTests, AttachmentElementInsertion)
 {
     auto webView = webViewForTestingAttachments();
+    RetainPtr<_WKAttachment> firstAttachment;
+    RetainPtr<_WKAttachment> secondAttachment;
+    {
+        ObserveAttachmentUpdatesForScope observer(webView.get());
+        // Use the given content type for the attachment element's type.
+        firstAttachment = retainPtr([webView synchronouslyInsertAttachmentWithFilename:@"foo" contentType:@"text/html" data:testHTMLData() options:nil]);
+        EXPECT_WK_STREQ(@"foo", [webView valueOfAttribute:@"title" forQuerySelector:@"attachment"]);
+        EXPECT_WK_STREQ(@"text/html", [webView valueOfAttribute:@"type" forQuerySelector:@"attachment"]);
+        EXPECT_WK_STREQ(@"38 bytes", [webView valueOfAttribute:@"subtitle" forQuerySelector:@"attachment"]);
+        observer.expectAttachmentUpdates(@[ ], @[ firstAttachment.get() ]);
+    }
+    {
+        ObserveAttachmentUpdatesForScope scope(webView.get());
+        // Since no content type is explicitly specified, compute it from the file extension.
+        [webView _executeEditCommand:@"DeleteBackward" argument:nil completion:nil];
+        secondAttachment = retainPtr([webView synchronouslyInsertAttachmentWithFilename:@"bar.png" contentType:nil data:testImageData() options:nil]);
+        EXPECT_WK_STREQ(@"bar.png", [webView valueOfAttribute:@"title" forQuerySelector:@"attachment"]);
+        EXPECT_WK_STREQ(@"image/png", [webView valueOfAttribute:@"type" forQuerySelector:@"attachment"]);
+        EXPECT_WK_STREQ(@"37 KB", [webView valueOfAttribute:@"subtitle" forQuerySelector:@"attachment"]);
+        scope.expectAttachmentUpdates(@[ firstAttachment.get() ], @[ secondAttachment.get() ]);
+    }
+}
+
+TEST(WKAttachmentTests, AttachmentUpdatesWhenInsertingAndDeletingNewline)
+{
+    auto webView = webViewForTestingAttachments();
+    RetainPtr<_WKAttachment> attachment;
+    {
+        ObserveAttachmentUpdatesForScope observer(webView.get());
+        attachment = retainPtr([webView synchronouslyInsertAttachmentWithFilename:@"foo.txt" contentType:@"text/plain" data:testHTMLData() options:nil]);
+        observer.expectAttachmentUpdates(@[ ], @[attachment.get()]);
+    }
+    [webView expectUpdatesAfterCommand:@"InsertParagraph" withArgument:nil expectedRemovals:@[] expectedInsertions:@[]];
+    [webView expectUpdatesAfterCommand:@"DeleteBackward" withArgument:nil expectedRemovals:@[] expectedInsertions:@[]];
+    [webView stringByEvaluatingJavaScript:@"getSelection().collapse(document.body)"];
+    [webView expectUpdatesAfterCommand:@"InsertParagraph" withArgument:nil expectedRemovals:@[] expectedInsertions:@[]];
+    [webView expectUpdatesAfterCommand:@"DeleteForward" withArgument:nil expectedRemovals:@[attachment.get()] expectedInsertions:@[]];
+}
 
-    // Use the given content type for the attachment element's type.
-    [webView synchronouslyInsertAttachmentWithFilename:@"foo" contentType:@"text/html" data:testHTMLData() options:nil];
-    EXPECT_WK_STREQ(@"foo", [webView valueOfAttribute:@"title" forQuerySelector:@"attachment"]);
-    EXPECT_WK_STREQ(@"text/html", [webView valueOfAttribute:@"type" forQuerySelector:@"attachment"]);
-    EXPECT_WK_STREQ(@"38 bytes", [webView valueOfAttribute:@"subtitle" forQuerySelector:@"attachment"]);
-
-    // Since no content type is explicitly specified, compute it from the file extension.
-    [webView _executeEditCommand:@"DeleteBackward" argument:nil completion:nil];
-    [webView synchronouslyInsertAttachmentWithFilename:@"bar.png" contentType:nil data:testImageData() options:nil];
-    EXPECT_WK_STREQ(@"bar.png", [webView valueOfAttribute:@"title" forQuerySelector:@"attachment"]);
-    EXPECT_WK_STREQ(@"image/png", [webView valueOfAttribute:@"type" forQuerySelector:@"attachment"]);
-    EXPECT_WK_STREQ(@"37 KB", [webView valueOfAttribute:@"subtitle" forQuerySelector:@"attachment"]);
+TEST(WKAttachmentTests, AttachmentUpdatesWhenUndoingAndRedoing)
+{
+    auto webView = webViewForTestingAttachments();
+    RetainPtr<_WKAttachment> attachment;
+    {
+        ObserveAttachmentUpdatesForScope observer(webView.get());
+        attachment = retainPtr([webView synchronouslyInsertAttachmentWithFilename:@"foo.txt" contentType:@"text/plain" data:testHTMLData() options:nil]);
+        observer.expectAttachmentUpdates(@[ ], @[attachment.get()]);
+    }
+    [webView expectUpdatesAfterCommand:@"Undo" withArgument:nil expectedRemovals:@[attachment.get()] expectedInsertions:@[]];
+    [webView expectUpdatesAfterCommand:@"Redo" withArgument:nil expectedRemovals:@[] expectedInsertions:@[attachment.get()]];
+    [webView expectUpdatesAfterCommand:@"DeleteBackward" withArgument:nil expectedRemovals:@[attachment.get()] expectedInsertions:@[]];
+    [webView expectUpdatesAfterCommand:@"Undo" withArgument:nil expectedRemovals:@[] expectedInsertions:@[attachment.get()]];
+    [webView expectUpdatesAfterCommand:@"Redo" withArgument:nil expectedRemovals:@[attachment.get()] expectedInsertions:@[]];
+}
+
+TEST(WKAttachmentTests, AttachmentUpdatesWhenChangingFontStyles)
+{
+    auto webView = webViewForTestingAttachments();
+    RetainPtr<_WKAttachment> attachment;
+    [webView _synchronouslyExecuteEditCommand:@"InsertText" argument:@"Hello"];
+    {
+        ObserveAttachmentUpdatesForScope observer(webView.get());
+        attachment = retainPtr([webView synchronouslyInsertAttachmentWithFilename:@"foo.txt" contentType:@"text/plain" data:testHTMLData() options:nil]);
+        observer.expectAttachmentUpdates(@[ ], @[attachment.get()]);
+    }
+    [webView expectUpdatesAfterCommand:@"InsertText" withArgument:@"World" expectedRemovals:@[] expectedInsertions:@[]];
+    [webView _synchronouslyExecuteEditCommand:@"SelectAll" argument:nil];
+    [webView expectUpdatesAfterCommand:@"ToggleBold" withArgument:nil expectedRemovals:@[] expectedInsertions:@[]];
+    [webView expectUpdatesAfterCommand:@"ToggleItalic" withArgument:nil expectedRemovals:@[] expectedInsertions:@[]];
+    [webView expectUpdatesAfterCommand:@"ToggleUnderline" withArgument:nil expectedRemovals:@[] expectedInsertions:@[]];
+
+    // Inserting text should delete the current selection, removing the attachment in the process.
+    [webView expectUpdatesAfterCommand:@"InsertText" withArgument:@"foo" expectedRemovals:@[attachment.get()] expectedInsertions:@[]];
+}
+
+TEST(WKAttachmentTests, AttachmentUpdatesWhenInsertingLists)
+{
+    auto webView = webViewForTestingAttachments();
+    RetainPtr<_WKAttachment> attachment;
+    {
+        ObserveAttachmentUpdatesForScope observer(webView.get());
+        attachment = retainPtr([webView synchronouslyInsertAttachmentWithFilename:@"foo.txt" contentType:@"text/plain" data:testHTMLData() options:nil]);
+        observer.expectAttachmentUpdates(@[ ], @[attachment.get()]);
+        NSLog(@"The markup is now %@", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
+    }
+    [webView expectUpdatesAfterCommand:@"InsertOrderedList" withArgument:nil expectedRemovals:@[] expectedInsertions:@[]];
+    // This edit command behaves more like a "toggle", and will actually break us out of the list we just inserted.
+    [webView expectUpdatesAfterCommand:@"InsertOrderedList" withArgument:nil expectedRemovals:@[] expectedInsertions:@[]];
+    [webView expectUpdatesAfterCommand:@"InsertUnorderedList" withArgument:nil expectedRemovals:@[] expectedInsertions:@[]];
+    [webView expectUpdatesAfterCommand:@"InsertUnorderedList" withArgument:nil expectedRemovals:@[] expectedInsertions:@[]];
+}
+
+TEST(WKAttachmentTests, AttachmentUpdatesWhenInsertingRichMarkup)
+{
+    auto webView = webViewForTestingAttachments();
+    RetainPtr<_WKAttachment> attachment;
+    {
+        ObserveAttachmentUpdatesForScope observer(webView.get());
+        [webView _synchronouslyExecuteEditCommand:@"InsertHTML" argument:@"<div><strong><attachment title='a' webkitattachmentid='a06fec41-9aa0-4c2c-ba3a-0149b54aad99'></attachment></strong></div>"];
+        attachment = observer.observer().inserted[0];
+        observer.expectAttachmentUpdates(@[ ], @[attachment.get()]);
+    }
+    {
+        ObserveAttachmentUpdatesForScope observer(webView.get());
+        [webView stringByEvaluatingJavaScript:@"document.querySelector('attachment').remove()"];
+        [webView waitForNextPresentationUpdate];
+        observer.expectAttachmentUpdates(@[attachment.get()], @[ ]);
+    }
 }
 
 } // namespace TestWebKitAPI