diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js')
-rw-r--r-- | widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js | 616 |
1 files changed, 616 insertions, 0 deletions
diff --git a/widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js b/widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js new file mode 100644 index 0000000000..9f1ab2d305 --- /dev/null +++ b/widget/tests/file_test_ime_state_in_contenteditable_on_readonly_change.js @@ -0,0 +1,616 @@ +/* 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 */ + +class IMEStateInContentEditableOnReadonlyChangeTester { + // Runner only fields. + #mEditingHost; + #mFocusElement; + #mWindow; + + // Tester only fields. + #mTIPWrapper; + #mWindowUtils; + + clear() { + this.#mTIPWrapper?.clearFocusBlurNotifications(); + this.#mTIPWrapper = null; + } + + #flushPendingIMENotifications() { + return new Promise(resolve => + this.#mWindow.requestAnimationFrame(() => + this.#mWindow.requestAnimationFrame(resolve) + ) + ); + } + + #getExpectedIMEState() { + // Although if this.#mFocusElement is a <button>, its `.focus()` call + // focus it, but caret is not set into it and following typing is handled + // outside the <button>. Therefore, anyway the enabled state should be + // "enabled". + return SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED; + } + + /** + * @param {Element} aEditingHost The editing host. + * @param {Element} aFocusElement Element which should have focus. This must + * be an inclusive descendant of the editing host and editable element. + * @param {Window} aWindow [optional] The window. + * @returns {object} Expected result of initial state. + */ + async prepareToRun(aEditingHost, aFocusElement, aWindow = window) { + this.#mWindow = aWindow; + this.#mEditingHost = aEditingHost; + this.#mFocusElement = aFocusElement; + + if (this.#mEditingHost.ownerDocument.activeElement) { + this.#mEditingHost.ownerDocument.activeElement.blur(); + await this.#flushPendingIMENotifications(); + } + + this.#mWindow.focus(); + this.#mEditingHost.setAttribute("contenteditable", ""); + this.#mFocusElement.focus(); + + await this.#flushPendingIMENotifications(); + + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: `when initialized with setting focus to ${ + this.#mFocusElement == this.#mEditingHost + ? "the editing host" + : `<${this.#mFocusElement.tagName.toLowerCase()}>` + }`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + /** + * @param {object} aExpectedResult The expected result of the test. + */ + #checkResult(aExpectedResult) { + const description = `IMEStateInContentEditableOnReadonlyChangeTester`; + is( + this.#mWindowUtils.IMEStatus, + aExpectedResult.expectedIMEState, + `${description}: IME enabled state should be expected one ${aExpectedResult.description}` + ); + is( + this.#mTIPWrapper.IMEHasFocus, + aExpectedResult.expectedIMEFocus, + `${description}: IME should ${ + aExpectedResult.expectedIMEFocus ? "" : "not " + }have focus ${aExpectedResult.description}` + ); + } + + /** + * @param {object} aExpectedResult The expected result of prepareToRun(). + * @param {Window} aWindow The window to check IME state. + * @param {TIPWrapper} aTIPWrapper The TIPWrapper for aWindow. + */ + checkResultOfPreparation(aExpectedResult, aWindow, aTIPWrapper) { + this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils; + this.#mTIPWrapper = aTIPWrapper; + this.#checkResult(aExpectedResult); + } + + /** + * @returns {object} The expected result. + */ + async runToMakeHTMLEditorReadonly() { + const htmlEditor = SpecialPowers.wrap(this.#mWindow).docShell.editor; + htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + + await this.#flushPendingIMENotifications(); + + return { + description: + this.#mFocusElement == this.#mEditingHost + ? "when the editing host has focus" + : `when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + expectedIMEFocus: false, + }; + } + + /** + * @param {object} aExpectedResult The expected result of runToMakeHTMLEditorReadonly(). + */ + checkResultOfMakingHTMLEditorReadonly(aExpectedResult) { + this.#checkResult(aExpectedResult); + } + + /** + * @returns {object} The expected result. + */ + async runToMakeHTMLEditorEditable() { + const htmlEditor = SpecialPowers.wrap(this.#mWindow).docShell.editor; + htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + + await this.#flushPendingIMENotifications(); + + const expectedIMEState = this.#getExpectedIMEState(); + return { + description: + this.#mFocusElement == this.#mEditingHost + ? "when the editing host has focus" + : `when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`, + expectedIMEState, + expectedIMEFocus: + expectedIMEState != + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + }; + } + + /** + * @param {object} aExpectedResult The expected result of runToMakeHTMLEditorEditable(). + */ + checkResultOfMakingHTMLEditorEditable(aExpectedResult) { + this.#checkResult(aExpectedResult); + } + + async runToRemoveContentEditableAttribute() { + this.#mEditingHost.removeAttribute("contenteditable"); + + await this.#flushPendingIMENotifications(); + + return { + description: + this.#mFocusElement == this.#mEditingHost + ? "after removing contenteditable attribute when the editing host has focus" + : `after removing contenteditable attribute when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`, + expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + expectedIMEFocus: false, + }; + } + + /** + * @param {object} aExpectedResult The expected result of runToRemoveContentEditableAttribute(). + */ + checkResultOfRemovingContentEditableAttribute(aExpectedResult) { + this.#checkResult(aExpectedResult); + } +} + +class IMEStateOfTextControlInContentEditableOnReadonlyChangeTester { + static #sTextControls = [ + { + tag: "input", + type: "text", + readonly: false, + }, + { + tag: "input", + type: "text", + readonly: true, + }, + { + tag: "textarea", + readonly: false, + }, + { + tag: "textarea", + readonly: true, + }, + ]; + + static get numberOfTextControlTypes() { + return IMEStateOfTextControlInContentEditableOnReadonlyChangeTester + .#sTextControls.length; + } + + static #createElement(aDocument, aTextControl) { + const textControl = aDocument.createElement(aTextControl.tag); + if (aTextControl.type !== undefined) { + textControl.setAttribute("type", aTextControl.type); + } + if (aTextControl.readonly) { + textControl.setAttribute("readonly", ""); + } + return textControl; + } + + #getDescription() { + return `<${this.#mTextControl.tag}${ + this.#mTextControl.type !== undefined + ? ` type=${this.#mTextControl.type}` + : "" + }${this.#mTextControl.readonly ? " readonly" : ""}>`; + } + + #getExpectedIMEState() { + return this.#mTextControl.readonly + ? 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. + #mEditingHost; + #mTextControl; + #mTextControlElement; + #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 which will have a text control. + * @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.ownerDocument.activeElement?.blur(); + this.#mEditingHost.removeAttribute("contenteditable"); + this.#mTextControlElement?.remove(); + await this.#flushPendingIMENotifications(); + this.#mTextControl = + IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.#sTextControls[ + aIndex + ]; + this.#mTextControlElement = + IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.#createElement( + this.#mEditingHost.ownerDocument, + this.#mTextControl + ); + this.#mEditingHost.appendChild(this.#mTextControlElement); + this.#mTextControlElement.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 = + "IMEStateOfTextControlInContentEditableOnReadonlyChangeTester"; + is( + this.#mWindowUtils.IMEStatus, + aExpectedResult.expectedIMEState, + `${description}: IME state should be proper one for the text control ${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); + } +} + +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); + } +} |