Bug 1806756 - part 8: Rewrite IME state tests of an element outside editing host to make it possible to run in remote content r=m_kato

Differential Revision: https://phabricator.services.mozilla.com/D171203
This commit is contained in:
Masayuki Nakano 2023-04-11 23:26:06 +00:00
parent 6883374a12
commit a1af67c53e
5 changed files with 316 additions and 87 deletions

View File

@ -201,6 +201,79 @@ add_task(async function() {
tester.clear();
}
})();
await (async function test_ime_state_outside_contenteditable_on_readonly_change() {
const tester = new IMEStateOutsideContentEditableOnReadonlyChangeTester();
await SpecialPowers.spawn(browser, [], () => {
content.document.body.innerHTML = "<div contenteditable></div>";
content.wrappedJSObject.runner = content.wrappedJSObject.createIMEStateOutsideContentEditableOnReadonlyChangeTester();
});
for (
let index = 0;
index <
IMEStateOutsideContentEditableOnReadonlyChangeTester.numberOfFocusTargets;
index++
) {
const expectedDataOfInitialization = await SpecialPowers.spawn(
browser,
[index],
aIndex => {
const editingHost = content.document.querySelector("div");
return content.wrappedJSObject.runner.prepareToRun(
aIndex,
editingHost,
content.window
);
}
);
tester.checkResultOfPreparation(
expectedDataOfInitialization,
window,
tipWrapper
);
const expectedDataOfMakingParentEditingHost = await SpecialPowers.spawn(
browser,
[],
() => {
return content.wrappedJSObject.runner.runToMakeParentEditingHost();
}
);
tester.checkResultOfMakingParentEditingHost(
expectedDataOfMakingParentEditingHost
);
const expectedDataOfMakingHTMLEditorReadonly = await SpecialPowers.spawn(
browser,
[],
() => {
return content.wrappedJSObject.runner.runToMakeHTMLEditorReadonly();
}
);
tester.checkResultOfMakingHTMLEditorReadonly(
expectedDataOfMakingHTMLEditorReadonly
);
const expectedDataOfMakingHTMLEditorEditable = await SpecialPowers.spawn(
browser,
[],
() => {
return content.wrappedJSObject.runner.runToMakeHTMLEditorEditable();
}
);
tester.checkResultOfMakingHTMLEditorEditable(
expectedDataOfMakingHTMLEditorEditable
);
const expectedDataOfMakingParentNonEditable = await SpecialPowers.spawn(
browser,
[],
() => {
return content.wrappedJSObject.runner.runToMakeParentNonEditingHost();
}
);
tester.checkResultOfMakingParentNonEditable(
expectedDataOfMakingParentNonEditable
);
tester.clear();
}
})();
}
);
});

View File

@ -22,6 +22,9 @@ function createIMEStateInContentEditableOnReadonlyChangeTester() {
function createIMEStateOfTextControlInContentEditableOnReadonlyChangeTester() {
return new IMEStateOfTextControlInContentEditableOnReadonlyChangeTester();
}
function createIMEStateOutsideContentEditableOnReadonlyChangeTester() {
return new IMEStateOutsideContentEditableOnReadonlyChangeTester();
}
function createIMEStateWhenNoActiveElementTester(aDescription) {
return new IMEStateWhenNoActiveElementTester(aDescription);
}

View File

@ -388,3 +388,227 @@ class IMEStateOfTextControlInContentEditableOnReadonlyChangeTester {
this.#checkResult(aExpectedResult);
}
}
class IMEStateOutsideContentEditableOnReadonlyChangeTester {
static #sFocusTargets = [
{
tag: "input",
type: "text",
readonly: false,
},
{
tag: "input",
type: "text",
readonly: true,
},
{
tag: "textarea",
readonly: false,
},
{
tag: "textarea",
readonly: true,
},
{
tag: "button",
},
{
tag: "body",
},
];
static get numberOfFocusTargets() {
return IMEStateOutsideContentEditableOnReadonlyChangeTester.#sFocusTargets
.length;
}
static #maybeCreateElement(aDocument, aFocusTarget) {
if (aFocusTarget.tag == "body") {
return null;
}
const element = aDocument.createElement(aFocusTarget.tag);
if (aFocusTarget.type !== undefined) {
element.setAttribute("type", aFocusTarget.type);
}
if (aFocusTarget.readonly) {
element.setAttribute("readonly", "");
}
return element;
}
#getDescription() {
return `<${this.#mFocusTarget.tag}${
this.#mFocusTarget.type !== undefined
? ` type=${this.#mFocusTarget.type}`
: ""
}${this.#mFocusTarget.readonly ? " readonly" : ""}>`;
}
#getExpectedIMEState() {
return this.#mFocusTarget.readonly ||
this.#mFocusTarget.tag == "button" ||
this.#mFocusTarget.tag == "body"
? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED
: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
}
#flushPendingIMENotifications() {
return new Promise(resolve =>
this.#mWindow.requestAnimationFrame(() =>
this.#mWindow.requestAnimationFrame(resolve)
)
);
}
// Runner only fields.
#mBody;
#mEditingHost;
#mFocusTarget;
#mFocusTargetElement;
#mWindow;
// Checker only fields.
#mWindowUtils;
#mTIPWrapper;
clear() {
this.#mTIPWrapper?.clearFocusBlurNotifications();
this.#mTIPWrapper = null;
}
/**
* @param {number} aIndex Index of the test.
* @param {Element} aEditingHost The editing host.
* @param {Window} aWindow [optional] The DOM window containing aEditingHost.
* @returns {object} Expected result of initial state.
*/
async prepareToRun(aIndex, aEditingHost, aWindow = window) {
this.#mWindow = aWindow;
this.#mEditingHost = aEditingHost;
this.#mEditingHost.removeAttribute("contenteditable");
this.#mBody = this.#mEditingHost.ownerDocument.body;
this.#mBody.ownerDocument.activeElement?.blur();
if (this.#mFocusTargetElement != this.#mBody) {
this.#mFocusTargetElement?.remove();
}
await this.#flushPendingIMENotifications();
this.#mFocusTarget =
IMEStateOutsideContentEditableOnReadonlyChangeTester.#sFocusTargets[
aIndex
];
this.#mFocusTargetElement = IMEStateOutsideContentEditableOnReadonlyChangeTester.#maybeCreateElement(
this.#mBody.ownerDocument,
this.#mFocusTarget
);
if (this.#mFocusTargetElement) {
this.#mBody.appendChild(this.#mFocusTargetElement);
this.#mFocusTargetElement.focus();
}
await this.#flushPendingIMENotifications();
const expectedIMEState = this.#getExpectedIMEState();
return {
description: `when ${this.#getDescription()} simply has focus`,
expectedIMEState,
expectedIMEFocus:
expectedIMEState !=
SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
};
}
#checkResult(aExpectedResult) {
const description = "IMEStateOutsideContentEditableOnReadonlyChangeTester";
is(
this.#mWindowUtils.IMEStatus,
aExpectedResult.expectedIMEState,
`${description}: IME state should be proper one for the focused element ${aExpectedResult.description}`
);
is(
this.#mTIPWrapper.IMEHasFocus,
aExpectedResult.expectedIMEFocus,
`${description}: IME should ${
aExpectedResult.expectedIMEFocus ? "" : "not "
}have focus ${aExpectedResult.description}`
);
}
/**
* @param {object} aExpectedResult The expected result returned by prepareToRun().
* @param {Window} aWindow The window whose IME state should be checked.
* @param {TIPWrapper} aTIPWrapper The TIP wrapper of aWindow.
*/
checkResultOfPreparation(aExpectedResult, aWindow, aTIPWrapper) {
this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils;
this.#mTIPWrapper = aTIPWrapper;
this.#checkResult(aExpectedResult);
}
async runToMakeParentEditingHost() {
this.#mEditingHost.setAttribute("contenteditable", "");
await this.#flushPendingIMENotifications();
const expectedIMEState = this.#getExpectedIMEState();
return {
description: `when parent of ${this.#getDescription()} becomes contenteditable`,
expectedIMEState,
expectedIMEFocus:
expectedIMEState !=
SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
};
}
checkResultOfMakingParentEditingHost(aExpectedResult) {
this.#checkResult(aExpectedResult);
}
async runToMakeHTMLEditorReadonly() {
const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
await this.#flushPendingIMENotifications();
const expectedIMEState = this.#getExpectedIMEState();
return {
description: `when HTMLEditor for parent of ${this.#getDescription()} becomes readonly`,
expectedIMEState,
expectedIMEFocus:
expectedIMEState !=
SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
};
}
checkResultOfMakingHTMLEditorReadonly(aExpectedResult) {
this.#checkResult(aExpectedResult);
}
async runToMakeHTMLEditorEditable() {
const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
editor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
await this.#flushPendingIMENotifications();
const expectedIMEState = this.#getExpectedIMEState();
return {
description: `when HTMLEditor for parent of ${this.#getDescription()} becomes editable`,
expectedIMEState,
expectedIMEFocus:
expectedIMEState !=
SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
};
}
checkResultOfMakingHTMLEditorEditable(aExpectedResult) {
this.#checkResult(aExpectedResult);
}
async runToMakeParentNonEditingHost() {
this.#mEditingHost.removeAttribute("contenteditable");
await this.#flushPendingIMENotifications();
const expectedIMEState = this.#getExpectedIMEState();
return {
description: `when parent of ${this.#getDescription()} becomes non-editable`,
expectedIMEState,
expectedIMEFocus:
expectedIMEState !=
SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
};
}
checkResultOfMakingParentNonEditable(aExpectedResult) {
this.#checkResult(aExpectedResult);
}
}

View File

@ -49,6 +49,21 @@ SimpleTest.waitForFocus(async () => {
editingHost.setAttribute("contenteditable", "");
})();
await (async function test_ime_state_outside_contenteditable_on_readonly_change() {
const tester = new IMEStateOutsideContentEditableOnReadonlyChangeTester();
for (let index = 0;
index < IMEStateOutsideContentEditableOnReadonlyChangeTester.numberOfFocusTargets;
index++) {
tester.checkResultOfPreparation(await tester.prepareToRun(index, editingHost), window, tipWrapper);
tester.checkResultOfMakingParentEditingHost(await tester.runToMakeParentEditingHost());
tester.checkResultOfMakingHTMLEditorReadonly(await tester.runToMakeHTMLEditorReadonly());
tester.checkResultOfMakingHTMLEditorEditable(await tester.runToMakeHTMLEditorEditable());
tester.checkResultOfMakingParentNonEditable(await tester.runToMakeParentNonEditingHost());
tester.clear();
}
editingHost.setAttribute("contenteditable", "");
})();
SimpleTest.finish();
});
</script>

View File

@ -9,17 +9,7 @@
</head>
<body onload="setTimeout(runTests, 0);" style="ime-mode: disabled;">
<div id="display" style="ime-mode: disabled;">
<!-- input elements -->
<input type="text" id="text"/><br/>
<input type="text" id="text_readonly" readonly="readonly"/><br/>
<!-- form controls -->
<button id="button">button</button><br/>
<textarea id="textarea">textarea</textarea><br/>
<textarea id="textarea_readonly" readonly="readonly">textarea[readonly]</textarea><br/>
<!-- contenteditable editor -->
<div id="contenteditableEditor" contenteditable="true"></div>
<input type="text" id="text"/><br/>
</div>
<div id="content" style="display: none">
@ -42,79 +32,6 @@ function hitEventLoop(aFunc, aTimes) {
var gUtils = window.windowUtils;
var gFM = Services.focus;
function runComplexContenteditableTests() {
const container = document.getElementById("display");
const kReadonly = Ci.nsIEditor.eEditorReadonlyMask;
function testOnOutsideOfEditor(aFocusNode, aFocusNodeDescription, aEditor) {
const description = "testOnOutsideOfEditor: ";
if (aFocusNode) {
aFocusNode.focus();
is(gFM.focusedElement, aFocusNode,
description + "The " + aFocusNodeDescription + " doesn't get focus");
} else {
if (document.activeElement) {
document.activeElement.blur();
}
is(gFM.focusedElement, null,
description + "Unexpected element has focus");
}
var expectedState =
aFocusNode ? gUtils.IMEStatus : gUtils.IME_STATUS_DISABLED;
var unexpectedStateDescription =
expectedState != gUtils.IME_STATUS_ENABLED ? "enabled" : "disabled";
aEditor.setAttribute("contenteditable", "true");
is(gFM.focusedElement, aFocusNode,
description + "The " + aFocusNodeDescription +
" loses focus, a HTML editor is editable now");
is(gUtils.IMEStatus, expectedState,
description + "IME becomes " + unexpectedStateDescription +
" on the " + aFocusNodeDescription +
", the HTML editor is editable now");
const editor = window.docShell.editor;
const flags = editor.flags;
editor.flags = flags | kReadonly;
is(gFM.focusedElement, aFocusNode,
description + aFocusNodeDescription +
" loses focus by changing HTML editor flags");
is(gUtils.IMEStatus, expectedState,
description + "IME becomes " + unexpectedStateDescription + " on " +
aFocusNodeDescription + ", the HTML editor is readonly now");
editor.flags = flags;
is(gFM.focusedElement, aFocusNode,
description + aFocusNodeDescription +
" loses focus by changing HTML editor flags #2");
is(gUtils.IMEStatus, expectedState,
description + "IME becomes " + unexpectedStateDescription + " on " +
aFocusNodeDescription + ", the HTML editor isn't readonly now");
container.removeAttribute("contenteditable");
is(gFM.focusedElement, aFocusNode,
description + aFocusNodeDescription +
" loses focus, the HTML editor has been no editable");
is(gUtils.IMEStatus, expectedState,
description + "IME becomes " + unexpectedStateDescription + " on " +
aFocusNodeDescription + ", the HTML editor has been no editable");
}
var div = document.getElementById("contenteditableEditor");
// a textarea which is outside of the editor has focus
testOnOutsideOfEditor(document.getElementById("textarea"), "textarea", div);
// a readonly textarea which is outside of the editor has focus
testOnOutsideOfEditor(document.getElementById("textarea_readonly"),
"textarea[readonly]", div);
// an input field which is outside of the editor has focus
testOnOutsideOfEditor(document.getElementById("text"),
"input[type=\"text\"]", div);
// a readonly input field which outside of the editor has focus
testOnOutsideOfEditor(document.getElementById("text_readonly"),
"input[type=\"text\"][readonly]", div);
// a readonly input field which outside of the editor has focus
testOnOutsideOfEditor(document.getElementById("button"), "button", div);
// nobody has focus.
testOnOutsideOfEditor(null, "nobody", div);
}
function runEditorFlagChangeTests() {
var description = "runEditorFlagChangeTests: ";
@ -332,9 +249,6 @@ async function runTests() {
await SpecialPowers.pushPrefEnv({
set: [["dom.forms.always_allow_key_and_focus_events.enabled", true]],
});
// complex contenteditable editor's tests
runComplexContenteditableTests();
// test whether the IME state and composition are not changed unexpectedly
runEditorFlagChangeTests();