Bug 1911010 - Make IMEContentObserver observe ParentChainChanged and let IMEStateManager know that r=smaug

`mozAutoDocUpdate` does not make it in a document change when container node
of `insertBefore` has already been removed from the tree.  Therefore, once
`IMEContentObserver::mRootElement` is removed from the DOM tree without a
focus move, `IMEContentObserver` is notified mutations not in a document change.

Similar situations are handled in `IMEStateManager::OnRemoveContent` with
emulating a focus change and that will destroy the active `IMEContentObserver`.
Therefore, if `IMEContentObserver::mRootElement` is removed, we should emulate
a focus move when `IMEStateManager` does not have focused element but there
is active `IMEContentObserver` (that means it is/was in the design mode).

However, checking whether the removed node contains the observing node of the
active `IMEContentObserver` may be expensive.  So, doing expensive things in
`IMEStateManager::OnRemoveContent` may make mutations slower.  Therefore, this
patch makes `IMEContentObserver` observe `ParentChainChanged` and it let
`IMEStateManager` know that with calling its
`OnParentChainChangedOfObservingElement`.  Finally, it calls
`IMEStateManager::OnRemoveContent` to emulate "blur" (and refocus if it's
required).

Differential Revision: https://phabricator.services.mozilla.com/D218696
This commit is contained in:
Masayuki Nakano 2024-08-27 07:55:26 +00:00
parent 72e9c67c58
commit b546eb7e32
11 changed files with 386 additions and 51 deletions

View File

@ -185,7 +185,7 @@ HTMLSlotElement* nsIContent::GetAssignedSlotByMode() const {
}
nsIContent::IMEState nsIContent::GetDesiredIMEState() {
if (!IsEditable()) {
if (!IsEditable() || !IsInComposedDoc()) {
// Check for the special case where we're dealing with elements which don't
// have the editable flag set, but are readwrite (such as text controls).
if (!IsElement() ||

View File

@ -591,46 +591,73 @@ nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell,
bool aAllowCrossShadowBoundary) {
NS_ENSURE_TRUE(aPresShell, nullptr);
if (IsDocument()) return AsDocument()->GetRootElement();
if (!IsContent()) return nullptr;
const bool isContent = IsContent();
if (GetComposedDoc() != aPresShell->GetDocument()) {
if (!isContent && !IsDocument()) {
return nullptr;
}
if (AsContent()->HasIndependentSelection() || IsInNativeAnonymousSubtree()) {
// This node should be an inclusive descendant of input/textarea editor.
// In that case, the anonymous <div> for TextEditor should be always the
// selection root.
// FIXME: If Selection for the document is collapsed in <input> or
// <textarea>, returning anonymous <div> may make the callers confused.
// Perhaps, we should do this only when this is in the native anonymous
// subtree unless the callers explicitly want to retrieve the anonymous
// <div> from a text control element.
if (Element* anonymousDivElement = GetAnonymousRootElementOfTextEditor()) {
return anonymousDivElement;
if (isContent) {
if (GetComposedDoc() != aPresShell->GetDocument()) {
return nullptr;
}
if (AsContent()->HasIndependentSelection() ||
IsInNativeAnonymousSubtree()) {
// This node should be an inclusive descendant of input/textarea editor.
// In that case, the anonymous <div> for TextEditor should be always the
// selection root.
// FIXME: If Selection for the document is collapsed in <input> or
// <textarea>, returning anonymous <div> may make the callers confused.
// Perhaps, we should do this only when this is in the native anonymous
// subtree unless the callers explicitly want to retrieve the anonymous
// <div> from a text control element.
if (Element* anonymousDivElement =
GetAnonymousRootElementOfTextEditor()) {
return anonymousDivElement;
}
}
}
nsPresContext* presContext = aPresShell->GetPresContext();
if (presContext) {
HTMLEditor* htmlEditor = nsContentUtils::GetHTMLEditor(presContext);
if (htmlEditor) {
// This node is in HTML editor.
if (nsPresContext* presContext = aPresShell->GetPresContext()) {
if (nsContentUtils::GetHTMLEditor(presContext)) {
// When there is an HTMLEditor, selection root should be one of focused
// editing host, <body> or root of the (sub)tree which this node belong.
// If this node is in design mode or this node is not editable, selection
// root should be the <body> if this node is not in any subtrees and there
// is a <body> or the root of the shadow DOM if this node is in a shadow
// or the document element.
// XXX If this node is not connected, it seems that this should return
// nullptr because this node is not selectable.
if (!IsInComposedDoc() || IsInDesignMode() ||
!HasFlag(NODE_IS_EDITABLE)) {
nsIContent* editorRoot = htmlEditor->GetRoot();
NS_ENSURE_TRUE(editorRoot, nullptr);
return nsContentUtils::IsInSameAnonymousTree(this, editorRoot)
? editorRoot
Element* const bodyOrDocumentElement = [&]() -> Element* {
if (Element* const bodyElement = OwnerDoc()->GetBodyElement()) {
return bodyElement;
}
return OwnerDoc()->GetDocumentElement();
}();
NS_ENSURE_TRUE(bodyOrDocumentElement, nullptr);
return nsContentUtils::IsInSameAnonymousTree(this,
bodyOrDocumentElement)
? bodyOrDocumentElement
: GetRootForContentSubtree(AsContent());
}
// If the document isn't editable but this is editable, this is in
// contenteditable. Use the editing host element for selection root.
// If this node is editable but not in the design mode, this is always an
// editable node in an editing host of contenteditable. In this case,
// let's use the editing host element as selection root.
MOZ_ASSERT(IsEditable());
MOZ_ASSERT(!IsInDesignMode());
MOZ_ASSERT(IsContent());
return static_cast<nsIContent*>(this)->GetEditingHost();
}
}
if (!isContent) {
return nullptr;
}
RefPtr<nsFrameSelection> fs = aPresShell->FrameSelection();
nsCOMPtr<nsIContent> content = fs->GetLimiter();
if (!content) {

View File

@ -213,6 +213,10 @@ void IMEContentObserver::OnIMEReceivedFocus() {
bool IMEContentObserver::InitWithEditor(nsPresContext& aPresContext,
Element* aElement,
EditorBase& aEditorBase) {
// mEditableNode is one of
// - Anonymous <div> in <input> or <textarea>
// - Editing host if it's not in the design mode
// - Document if it's in the design mode
mEditableNode = IMEStateManager::GetRootEditableNode(aPresContext, aElement);
if (NS_WARN_IF(!mEditableNode)) {
return false;
@ -260,11 +264,18 @@ bool IMEContentObserver::InitWithEditor(nsPresContext& aPresContext,
return false;
}
// If an editing host has focus, mRootElement is it.
// Otherwise, if we're in the design mode, mRootElement is the <body> if
// there is and startContainer is not outside of the <body>. Otherwise, the
// document element is used instead.
nsCOMPtr<nsINode> startContainer = selRange->GetStartContainer();
mRootElement = Element::FromNodeOrNull(
startContainer->GetSelectionRootContent(presShell));
} else {
MOZ_ASSERT(!mIsTextControl);
// If an editing host has focus, mRootElement is it.
// Otherwise, if we're in the design mode, mRootElement is the <body> if
// there is. Otherwise, the document element is used instead.
nsCOMPtr<nsINode> editableNode = mEditableNode;
mRootElement = Element::FromNodeOrNull(
editableNode->GetSelectionRootContent(presShell));
@ -325,6 +336,10 @@ void IMEContentObserver::ObserveEditableNode() {
mEditorBase->SetIMEContentObserver(this);
}
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p ObserveEditableNode(), starting to observe 0x%p (%s)", this,
mRootElement.get(), ToString(*mRootElement).c_str()));
mRootElement->AddMutationObserver(this);
// If it's in a document (should be so), we can use document observer to
// reduce redundant computation of text change offsets.
@ -380,6 +395,12 @@ void IMEContentObserver::UnregisterObservers() {
if (!mIsObserving) {
return;
}
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p UnregisterObservers(), stop observing 0x%p (%s)", this,
mRootElement.get(),
mRootElement ? ToString(*mRootElement).c_str() : "nullptr"));
mIsObserving = false;
if (mEditorBase) {
@ -1236,6 +1257,26 @@ void IMEContentObserver::ContentRemoved(nsIContent* aChild,
MaybeNotifyIMEOfTextChange(data);
}
MOZ_CAN_RUN_SCRIPT_BOUNDARY void IMEContentObserver::ParentChainChanged(
nsIContent* aContent) {
// When the observing element itself is directly removed from the document
// without a focus move, i.e., it's the root of the removed document fragment
// and the editor was handling the design mode, we have already stopped
// observing the element because IMEStateManager::OnRemoveContent() should
// have already been called for it and the instance which was observing the
// node has already been destroyed. Therefore, this is called only when
// this is observing the <body> in the design mode and it's disconnected from
// the tree by an <html> element removal. Even in this case, IMEStateManager
// never gets a focus change notification, but we need to notify IME of focus
// change because we cannot interact with IME anymore due to no editable
// content. Therefore, this method notifies IMEStateManager of the
// disconnection of the observing node to emulate a blur from the editable
// content.
MOZ_ASSERT(mIsObserving);
OwningNonNull<IMEContentObserver> observer(*this);
IMEStateManager::OnParentChainChangedOfObservingElement(observer);
}
void IMEContentObserver::OnTextControlValueChangedWhileNotObservable(
const nsAString& aNewValue) {
MOZ_ASSERT(mEditorBase);

View File

@ -15,6 +15,7 @@
#include "nsCOMPtr.h"
#include "nsCycleCollectionParticipant.h"
#include "nsIDocShell.h" // XXX Why does only this need to be included here?
#include "nsIMutationObserver.h"
#include "nsIReflowObserver.h"
#include "nsIScrollObserver.h"
#include "nsIWidget.h"
@ -59,6 +60,7 @@ class IMEContentObserver final : public nsStubMutationObserver,
NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
NS_DECL_NSIMUTATIONOBSERVER_PARENTCHAINCHANGED
NS_DECL_NSIREFLOWOBSERVER
// nsIScrollObserver

View File

@ -415,8 +415,7 @@ nsresult IMEStateManager::OnRemoveContent(nsPresContext& aPresContext,
if (compositionInContent) {
MOZ_LOG(sISMLog, LogLevel::Debug,
(" OnRemoveContent(), "
"composition is in the content"));
(" OnRemoveContent(), composition is in the content"));
// Try resetting the native IME state. Be aware, typically, this method
// is called during the content being removed. Then, the native
@ -430,8 +429,15 @@ nsresult IMEStateManager::OnRemoveContent(nsPresContext& aPresContext,
}
}
if (!sFocusedPresContext || !sFocusedElement ||
!sFocusedElement->IsInclusiveDescendantOf(&aElement)) {
if (!sFocusedPresContext ||
// If focused element is a text control or an editing host, we need to
// emulate "blur" on it when it's removed.
(sFocusedElement && sFocusedElement != &aElement) ||
// If it is (or was) in design mode, we need to emulate "blur" on the
// document when the observing element (typically, <body>) is removed.
(!sFocusedElement &&
(!sActiveIMEContentObserver ||
sActiveIMEContentObserver->GetObservingElement() != &aElement))) {
return NS_OK;
}
MOZ_ASSERT(sFocusedPresContext == &aPresContext);
@ -463,19 +469,48 @@ nsresult IMEStateManager::OnRemoveContent(nsPresContext& aPresContext,
SetIMEState(newState, &aPresContext, nullptr, textInputHandlingWidget, action,
origin);
if (sFocusedPresContext != &aPresContext || sFocusedElement) {
return NS_OK; // Some body must have set focus
return NS_OK; // Somebody already has focus, don't steal it.
}
if (IsIMEObserverNeeded(newState)) {
if (RefPtr<HTMLEditor> htmlEditor =
nsContentUtils::GetHTMLEditor(&aPresContext)) {
CreateIMEContentObserver(*htmlEditor, nullptr);
}
// Initializing IMEContentObserver instance requires Selection, but its
// ranges have not been adjusted for this removal. Therefore, we need to
// wait a moment.
nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
"IMEStateManager::RecreateIMEContentObserverWhenContentRemoved",
[presContext = OwningNonNull{aPresContext}]() {
MOZ_ASSERT(sFocusedPresContext == presContext);
MOZ_ASSERT(!sFocusedElement);
if (RefPtr<HTMLEditor> htmlEditor =
nsContentUtils::GetHTMLEditor(presContext)) {
CreateIMEContentObserver(*htmlEditor, nullptr);
}
}));
}
return NS_OK;
}
void IMEStateManager::OnParentChainChangedOfObservingElement(
IMEContentObserver& aObserver) {
if (!sFocusedPresContext || sActiveIMEContentObserver != &aObserver) {
return;
}
RefPtr<nsPresContext> presContext = aObserver.GetPresContext();
RefPtr<Element> element = aObserver.GetObservingElement();
if (NS_WARN_IF(!presContext) || NS_WARN_IF(!element)) {
return;
}
MOZ_LOG(sISMLog, LogLevel::Info,
("OnParentChainChangedOfObservingElement(aObserver=0x%p), "
"sFocusedPresContext=0x%p, sFocusedElement=0x%p, "
"aObserver->GetPresContext()=0x%p, "
"aObserver->GetObservingElement()=0x%p",
&aObserver, sFocusedPresContext.get(), sFocusedElement.get(),
presContext.get(), element.get()));
OnRemoveContent(*presContext, *element);
}
// static
bool IMEStateManager::CanHandleWith(const nsPresContext* aPresContext) {
return aPresContext && aPresContext->GetPresShell() &&

View File

@ -164,6 +164,13 @@ class IMEStateManager {
nsPresContext& aPresContext);
MOZ_CAN_RUN_SCRIPT static nsresult OnRemoveContent(
nsPresContext& aPresContext, dom::Element& aElement);
/**
* Called when the parent chain of the observing element of IMEContentObserver
* is changed.
*/
MOZ_CAN_RUN_SCRIPT static void OnParentChainChangedOfObservingElement(
IMEContentObserver& aObserver);
/**
* OnChangeFocus() should be called when focused content is changed or
* IME enabled state is changed. If nobody has focus, set both aPresContext

View File

@ -1059,10 +1059,10 @@ TextComposition* TextCompositionArray::GetCompositionFor(
TextComposition* TextCompositionArray::GetCompositionInContent(
nsPresContext* aPresContext, nsIContent* aContent) {
// There should be only one composition per content object.
for (index_type i = Length(); i > 0; --i) {
nsINode* node = ElementAt(i - 1)->GetEventTargetNode();
if (node && node->IsInclusiveDescendantOf(aContent)) {
return ElementAt(i - 1);
for (TextComposition* const composition : Reversed(*this)) {
nsINode* node = composition->GetEventTargetNode();
if (node && node->IsInclusiveFlatTreeDescendantOf(aContent)) {
return composition;
}
}
return nullptr;

View File

@ -825,8 +825,14 @@ nsresult HTMLEditor::FocusedElementOrDocumentBecomesNotEditable(
aHTMLEditor->mHasFocus = false;
aHTMLEditor->mIsInDesignMode = false;
const RefPtr<Element> focusedElement =
nsFocusManager::GetFocusedElementStatic();
RefPtr<Element> focusedElement = nsFocusManager::GetFocusedElementStatic();
if (focusedElement && !focusedElement->IsInComposedDoc()) {
// nsFocusManager may keep storing the focused element even after
// disconnected from the tree, but HTMLEditor cannot work with editable
// nodes not in a composed document. Therefore, we should treat no
// focused element in the case.
focusedElement = nullptr;
}
TextControlElement* const focusedTextControlElement =
TextControlElement::FromNodeOrNull(focusedElement);
if ((focusedElement && focusedElement->IsEditable() &&
@ -854,14 +860,19 @@ nsresult HTMLEditor::FocusedElementOrDocumentBecomesNotEditable(
// If the element becomes not editable without focus change, IMEStateManager
// does not have a chance to disable IME. Therefore, (even if we fail to
// handle the emulated blur/focus above,) we should notify IMEStateManager of
// the editing state change.
RefPtr<Element> focusedElement = nsFocusManager::GetFocusedElementStatic();
RefPtr<nsPresContext> presContext =
focusedElement ? focusedElement->GetPresContext(
Element::PresContextFor::eForComposedDoc)
: aDocument.GetPresContext();
if (presContext) {
IMEStateManager::MaybeOnEditableStateDisabled(*presContext, focusedElement);
// the editing state change. Note that if the window of the HTMLEditor has
// already lost focus, we don't need to do that and we should not touch the
// other windows.
if (aHTMLEditor->OurWindowHasFocus()) {
if (RefPtr<nsPresContext> presContext = aHTMLEditor->GetPresContext()) {
RefPtr<Element> focusedElement =
nsFocusManager::GetFocusedElementStatic();
MOZ_ASSERT_IF(focusedElement,
focusedElement->GetPresContext(
Element::PresContextFor::eForComposedDoc));
IMEStateManager::MaybeOnEditableStateDisabled(*presContext,
focusedElement);
}
}
return rv;
@ -878,10 +889,13 @@ nsresult HTMLEditor::OnBlur(const EventTarget* aEventTarget) {
? "true"
: "false")
: "N/A"));
const Element* eventTargetAsElement =
Element::FromEventTargetOrNull(aEventTarget);
// If another element already has focus, we should not maintain the selection
// because we may not have the rights doing it.
if (nsFocusManager::GetFocusedElementStatic()) {
const Element* focusedElement = nsFocusManager::GetFocusedElementStatic();
if (focusedElement && focusedElement != eventTargetAsElement) {
// XXX If we had focus and new focused element is a text control, we may
// need to notify focus of its TextEditor...
mIsInDesignMode = false;
@ -892,7 +906,8 @@ nsresult HTMLEditor::OnBlur(const EventTarget* aEventTarget) {
// If we're in the designMode and blur occurs, the target must be the document
// node. If a blur event is fired and the target is an element, it must be
// delayed blur event at initializing the `HTMLEditor`.
if (mIsInDesignMode && Element::FromEventTargetOrNull(aEventTarget)) {
if (mIsInDesignMode && eventTargetAsElement &&
eventTargetAsElement->IsInComposedDoc()) {
return NS_OK;
}

View File

@ -0,0 +1,37 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script>
"use strict";
document.addEventListener("DOMContentLoaded", async () => {
const q = document.querySelector("q");
const data = document.querySelector("data");
const meter = document.querySelector("meter");
const button = document.querySelector("button");
button.addEventListener(
"focusout",
() => document.createElement("a").append(q)
);
const promiseButtonFocus = new Promise(resolve => {
button.addEventListener("button", resolve, {once: true});
});
button.focus();
promiseButtonFocus;
document.designMode = "on";
q.insertBefore(document.body, data);
meter.insertBefore(data, meter.childNodes[0]);
}, {once: true});
</script>
</head>
<body>
<q>
<data>
</q>
<dialog>
<meter>
</dialog>
<button></button>
</body>
</html>

View File

@ -21,6 +21,9 @@ skip-if = ["os != 'win'"]
["browser_test_fullscreen_size.js"]
["browser_test_ime_state_after_body_removed_and_reconnected_in_designMode.js"]
support-files = [ "../file_ime_state_test_helper.js" ]
["browser_test_ime_state_in_contenteditable_on_focus_move_in_remote_content.js"]
support-files = [
"file_ime_state_tests.html",

View File

@ -0,0 +1,168 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* import-globals-from ../file_ime_state_test_helper.js */
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/widget/tests/browser/file_ime_state_test_helper.js",
this
);
add_task(async function test_replace_body_in_designMode() {
await BrowserTestUtils.withNewTab(
"data:text/html,<html><body><br></body></html>",
async function (browser) {
const tipWrapper = new TIPWrapper(window);
ok(
tipWrapper.isAvailable(),
"test_replace_body_in_designMode: TextInputProcessor should've been initialized"
);
function waitFor(aWaitingNotification) {
return new Promise(resolve => {
tipWrapper.onIMEFocusBlur = aNotification => {
if (aNotification != aWaitingNotification) {
return;
}
tipWrapper.onIMEFocusBlur = null;
resolve();
};
});
}
const waitForInitialFocus = waitFor("notify-focus");
await SpecialPowers.spawn(browser, [], () => {
content.document.designMode = "on";
content.document.body.focus();
});
info("test_replace_body_in_designMode: Waiting for initial IME focus...");
await waitForInitialFocus;
Assert.equal(
window.windowUtils.IMEStatus,
Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
"test_replace_body_in_designMode: IME should be enabled when the document becomes editable"
);
tipWrapper.clearFocusBlurNotifications();
const waitForRefocusAfterBodyRemoval = waitFor("notify-focus");
await SpecialPowers.spawn(browser, [], () => {
content.wrappedJSObject.body = content.document.body;
content.document.body.remove();
});
info(
"test_replace_body_in_designMode: Waiting for IME refocus after the <body> is removed..."
);
await waitForRefocusAfterBodyRemoval;
Assert.equal(
window.windowUtils.IMEStatus,
Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
"test_replace_body_in_designMode: IME should be enabled after the <body> is removed"
);
await SpecialPowers.spawn(browser, [], () => {
content.document.documentElement.appendChild(
content.wrappedJSObject.body
);
});
tipWrapper.clearFocusBlurNotifications();
await new Promise(resolve => {
window.requestAnimationFrame(() =>
window.requestAnimationFrame(resolve)
);
});
// FIXME: IME should be refocused when new <body> is inserted because
// HTMLEditor::GetRoot() returns the reconnected <body> now, but
// IMEContentObserver keeps observing the <html>.
Assert.equal(
tipWrapper.numberOfBlurNotifications +
tipWrapper.numberOfFocusNotifications,
0,
"test_replace_body_in_designMode: IME should not be refocused when the <body> is reconnected"
);
Assert.equal(
window.windowUtils.IMEStatus,
Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
"test_replace_body_in_designMode: IME should be enabled after the <body> is reconnected"
);
}
);
});
add_task(async function test_replace_document_element_in_designMode() {
await BrowserTestUtils.withNewTab(
"data:text/html,<html><body><br></body></html>",
async function (browser) {
const tipWrapper = new TIPWrapper(window);
ok(
tipWrapper.isAvailable(),
"test_replace_document_element_in_designMode: TextInputProcessor should've been initialized"
);
function waitFor(aWaitingNotification) {
return new Promise(resolve => {
tipWrapper.onIMEFocusBlur = aNotification => {
if (aNotification != aWaitingNotification) {
return;
}
tipWrapper.onIMEFocusBlur = null;
resolve();
};
});
}
const waitForInitialFocus = waitFor("notify-focus");
await SpecialPowers.spawn(browser, [], () => {
content.document.designMode = "on";
content.document.body.focus();
});
info(
"test_replace_document_element_in_designMode: Waiting for initial IME focus..."
);
await waitForInitialFocus;
Assert.equal(
window.windowUtils.IMEStatus,
Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
"test_replace_document_element_in_designMode: IME should be enabled when the document becomes editable"
);
tipWrapper.clearFocusBlurNotifications();
const waitForRefocusAfterDocumentElementRemoval = waitFor("notify-blur");
await SpecialPowers.spawn(browser, [], () => {
content.wrappedJSObject.documentElement =
content.document.documentElement;
content.document.documentElement.remove();
});
info(
"test_replace_document_element_in_designMode: Waiting for IME blur after the <html> is removed..."
);
await waitForRefocusAfterDocumentElementRemoval;
Assert.equal(
window.windowUtils.IMEStatus,
Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
"test_replace_document_element_in_designMode: IME should be enabled after the <html> is removed"
);
await SpecialPowers.spawn(browser, [], () => {
content.document.appendChild(content.wrappedJSObject.documentElement);
});
tipWrapper.clearFocusBlurNotifications();
await new Promise(resolve => {
window.requestAnimationFrame(() =>
window.requestAnimationFrame(resolve)
);
});
// FIXME: IME should be focused when new root element is inserted.
Assert.equal(
tipWrapper.numberOfBlurNotifications +
tipWrapper.numberOfFocusNotifications,
0,
"test_replace_document_element_in_designMode: IME should not be refocused when the <html> is reconnected"
);
Assert.equal(
window.windowUtils.IMEStatus,
Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED,
"test_replace_document_element_in_designMode: IME should be enabled after the <html> is reconnected"
);
}
);
});