Backed out changeset 2450366cf7ca (bug 1891629) for causing win msix mochitest failures
[gecko.git] / widget / tests / file_test_ime_state_in_contenteditable_on_readonly_change.js
blob9f1ab2d305ae971125e07a4300a2f7eafb934c89
1 /* Any copyright is dedicated to the Public Domain.
2    http://creativecommons.org/publicdomain/zero/1.0/ */
4 "use strict";
6 /* import-globals-from file_ime_state_test_helper.js */
8 class IMEStateInContentEditableOnReadonlyChangeTester {
9   // Runner only fields.
10   #mEditingHost;
11   #mFocusElement;
12   #mWindow;
14   // Tester only fields.
15   #mTIPWrapper;
16   #mWindowUtils;
18   clear() {
19     this.#mTIPWrapper?.clearFocusBlurNotifications();
20     this.#mTIPWrapper = null;
21   }
23   #flushPendingIMENotifications() {
24     return new Promise(resolve =>
25       this.#mWindow.requestAnimationFrame(() =>
26         this.#mWindow.requestAnimationFrame(resolve)
27       )
28     );
29   }
31   #getExpectedIMEState() {
32     // Although if this.#mFocusElement is a <button>, its `.focus()` call
33     // focus it, but caret is not set into it and following typing is handled
34     // outside the <button>.  Therefore, anyway the enabled state should be
35     // "enabled".
36     return SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
37   }
39   /**
40    * @param {Element} aEditingHost  The editing host.
41    * @param {Element} aFocusElement Element which should have focus.  This must
42    * be an inclusive descendant of the editing host and editable element.
43    * @param {Window} aWindow [optional] The window.
44    * @returns {object} Expected result of initial state.
45    */
46   async prepareToRun(aEditingHost, aFocusElement, aWindow = window) {
47     this.#mWindow = aWindow;
48     this.#mEditingHost = aEditingHost;
49     this.#mFocusElement = aFocusElement;
51     if (this.#mEditingHost.ownerDocument.activeElement) {
52       this.#mEditingHost.ownerDocument.activeElement.blur();
53       await this.#flushPendingIMENotifications();
54     }
56     this.#mWindow.focus();
57     this.#mEditingHost.setAttribute("contenteditable", "");
58     this.#mFocusElement.focus();
60     await this.#flushPendingIMENotifications();
62     const expectedIMEState = this.#getExpectedIMEState();
63     return {
64       description: `when initialized with setting focus to ${
65         this.#mFocusElement == this.#mEditingHost
66           ? "the editing host"
67           : `<${this.#mFocusElement.tagName.toLowerCase()}>`
68       }`,
69       expectedIMEState,
70       expectedIMEFocus:
71         expectedIMEState !=
72         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
73     };
74   }
76   /**
77    * @param {object} aExpectedResult The expected result of the test.
78    */
79   #checkResult(aExpectedResult) {
80     const description = `IMEStateInContentEditableOnReadonlyChangeTester`;
81     is(
82       this.#mWindowUtils.IMEStatus,
83       aExpectedResult.expectedIMEState,
84       `${description}: IME enabled state should be expected one ${aExpectedResult.description}`
85     );
86     is(
87       this.#mTIPWrapper.IMEHasFocus,
88       aExpectedResult.expectedIMEFocus,
89       `${description}: IME should ${
90         aExpectedResult.expectedIMEFocus ? "" : "not "
91       }have focus ${aExpectedResult.description}`
92     );
93   }
95   /**
96    * @param {object} aExpectedResult The expected result of prepareToRun().
97    * @param {Window} aWindow The window to check IME state.
98    * @param {TIPWrapper} aTIPWrapper The TIPWrapper for aWindow.
99    */
100   checkResultOfPreparation(aExpectedResult, aWindow, aTIPWrapper) {
101     this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils;
102     this.#mTIPWrapper = aTIPWrapper;
103     this.#checkResult(aExpectedResult);
104   }
106   /**
107    * @returns {object} The expected result.
108    */
109   async runToMakeHTMLEditorReadonly() {
110     const htmlEditor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
111     htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
113     await this.#flushPendingIMENotifications();
115     return {
116       description:
117         this.#mFocusElement == this.#mEditingHost
118           ? "when the editing host has focus"
119           : `when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`,
120       expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
121       expectedIMEFocus: false,
122     };
123   }
125   /**
126    * @param {object} aExpectedResult The expected result of runToMakeHTMLEditorReadonly().
127    */
128   checkResultOfMakingHTMLEditorReadonly(aExpectedResult) {
129     this.#checkResult(aExpectedResult);
130   }
132   /**
133    * @returns {object} The expected result.
134    */
135   async runToMakeHTMLEditorEditable() {
136     const htmlEditor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
137     htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
139     await this.#flushPendingIMENotifications();
141     const expectedIMEState = this.#getExpectedIMEState();
142     return {
143       description:
144         this.#mFocusElement == this.#mEditingHost
145           ? "when the editing host has focus"
146           : `when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`,
147       expectedIMEState,
148       expectedIMEFocus:
149         expectedIMEState !=
150         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
151     };
152   }
154   /**
155    * @param {object} aExpectedResult The expected result of runToMakeHTMLEditorEditable().
156    */
157   checkResultOfMakingHTMLEditorEditable(aExpectedResult) {
158     this.#checkResult(aExpectedResult);
159   }
161   async runToRemoveContentEditableAttribute() {
162     this.#mEditingHost.removeAttribute("contenteditable");
164     await this.#flushPendingIMENotifications();
166     return {
167       description:
168         this.#mFocusElement == this.#mEditingHost
169           ? "after removing contenteditable attribute when the editing host has focus"
170           : `after removing contenteditable attribute when <${this.#mFocusElement.tagName.toLowerCase()}> has focus`,
171       expectedIMEState: SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
172       expectedIMEFocus: false,
173     };
174   }
176   /**
177    * @param {object} aExpectedResult The expected result of runToRemoveContentEditableAttribute().
178    */
179   checkResultOfRemovingContentEditableAttribute(aExpectedResult) {
180     this.#checkResult(aExpectedResult);
181   }
184 class IMEStateOfTextControlInContentEditableOnReadonlyChangeTester {
185   static #sTextControls = [
186     {
187       tag: "input",
188       type: "text",
189       readonly: false,
190     },
191     {
192       tag: "input",
193       type: "text",
194       readonly: true,
195     },
196     {
197       tag: "textarea",
198       readonly: false,
199     },
200     {
201       tag: "textarea",
202       readonly: true,
203     },
204   ];
206   static get numberOfTextControlTypes() {
207     return IMEStateOfTextControlInContentEditableOnReadonlyChangeTester
208       .#sTextControls.length;
209   }
211   static #createElement(aDocument, aTextControl) {
212     const textControl = aDocument.createElement(aTextControl.tag);
213     if (aTextControl.type !== undefined) {
214       textControl.setAttribute("type", aTextControl.type);
215     }
216     if (aTextControl.readonly) {
217       textControl.setAttribute("readonly", "");
218     }
219     return textControl;
220   }
222   #getDescription() {
223     return `<${this.#mTextControl.tag}${
224       this.#mTextControl.type !== undefined
225         ? ` type=${this.#mTextControl.type}`
226         : ""
227     }${this.#mTextControl.readonly ? " readonly" : ""}>`;
228   }
230   #getExpectedIMEState() {
231     return this.#mTextControl.readonly
232       ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED
233       : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
234   }
236   #flushPendingIMENotifications() {
237     return new Promise(resolve =>
238       this.#mWindow.requestAnimationFrame(() =>
239         this.#mWindow.requestAnimationFrame(resolve)
240       )
241     );
242   }
244   // Runner only fields.
245   #mEditingHost;
246   #mTextControl;
247   #mTextControlElement;
248   #mWindow;
250   // Checker only fields.
251   #mWindowUtils;
252   #mTIPWrapper;
254   clear() {
255     this.#mTIPWrapper?.clearFocusBlurNotifications();
256     this.#mTIPWrapper = null;
257   }
259   /**
260    * @param {number} aIndex Index of the test.
261    * @param {Element} aEditingHost The editing host which will have a text control.
262    * @param {Window} aWindow [optional] The DOM window containing aEditingHost.
263    * @returns {object} Expected result of initial state.
264    */
265   async prepareToRun(aIndex, aEditingHost, aWindow = window) {
266     this.#mWindow = aWindow;
267     this.#mEditingHost = aEditingHost;
268     this.#mEditingHost.ownerDocument.activeElement?.blur();
269     this.#mEditingHost.removeAttribute("contenteditable");
270     this.#mTextControlElement?.remove();
271     await this.#flushPendingIMENotifications();
272     this.#mTextControl =
273       IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.#sTextControls[
274         aIndex
275       ];
276     this.#mTextControlElement =
277       IMEStateOfTextControlInContentEditableOnReadonlyChangeTester.#createElement(
278         this.#mEditingHost.ownerDocument,
279         this.#mTextControl
280       );
281     this.#mEditingHost.appendChild(this.#mTextControlElement);
282     this.#mTextControlElement.focus();
283     await this.#flushPendingIMENotifications();
284     const expectedIMEState = this.#getExpectedIMEState();
285     return {
286       description: `when ${this.#getDescription()} simply has focus`,
287       expectedIMEState,
288       expectedIMEFocus:
289         expectedIMEState !=
290         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
291     };
292   }
294   #checkResult(aExpectedResult) {
295     const description =
296       "IMEStateOfTextControlInContentEditableOnReadonlyChangeTester";
297     is(
298       this.#mWindowUtils.IMEStatus,
299       aExpectedResult.expectedIMEState,
300       `${description}: IME state should be proper one for the text control ${aExpectedResult.description}`
301     );
302     is(
303       this.#mTIPWrapper.IMEHasFocus,
304       aExpectedResult.expectedIMEFocus,
305       `${description}: IME should ${
306         aExpectedResult.expectedIMEFocus ? "" : "not "
307       }have focus ${aExpectedResult.description}`
308     );
309   }
311   /**
312    * @param {object} aExpectedResult The expected result returned by prepareToRun().
313    * @param {Window} aWindow The window whose IME state should be checked.
314    * @param {TIPWrapper} aTIPWrapper The TIP wrapper of aWindow.
315    */
316   checkResultOfPreparation(aExpectedResult, aWindow, aTIPWrapper) {
317     this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils;
318     this.#mTIPWrapper = aTIPWrapper;
319     this.#checkResult(aExpectedResult);
320   }
322   async runToMakeParentEditingHost() {
323     this.#mEditingHost.setAttribute("contenteditable", "");
324     await this.#flushPendingIMENotifications();
325     const expectedIMEState = this.#getExpectedIMEState();
326     return {
327       description: `when parent of ${this.#getDescription()} becomes contenteditable`,
328       expectedIMEState,
329       expectedIMEFocus:
330         expectedIMEState !=
331         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
332     };
333   }
335   checkResultOfMakingParentEditingHost(aExpectedResult) {
336     this.#checkResult(aExpectedResult);
337   }
339   async runToMakeHTMLEditorReadonly() {
340     const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
341     editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
342     await this.#flushPendingIMENotifications();
343     const expectedIMEState = this.#getExpectedIMEState();
344     return {
345       description: `when HTMLEditor for parent of ${this.#getDescription()} becomes readonly`,
346       expectedIMEState,
347       expectedIMEFocus:
348         expectedIMEState !=
349         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
350     };
351   }
353   checkResultOfMakingHTMLEditorReadonly(aExpectedResult) {
354     this.#checkResult(aExpectedResult);
355   }
357   async runToMakeHTMLEditorEditable() {
358     const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
359     editor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
360     await this.#flushPendingIMENotifications();
361     const expectedIMEState = this.#getExpectedIMEState();
362     return {
363       description: `when HTMLEditor for parent of ${this.#getDescription()} becomes editable`,
364       expectedIMEState,
365       expectedIMEFocus:
366         expectedIMEState !=
367         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
368     };
369   }
371   checkResultOfMakingHTMLEditorEditable(aExpectedResult) {
372     this.#checkResult(aExpectedResult);
373   }
375   async runToMakeParentNonEditingHost() {
376     this.#mEditingHost.removeAttribute("contenteditable");
377     await this.#flushPendingIMENotifications();
378     const expectedIMEState = this.#getExpectedIMEState();
379     return {
380       description: `when parent of ${this.#getDescription()} becomes non-editable`,
381       expectedIMEState,
382       expectedIMEFocus:
383         expectedIMEState !=
384         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
385     };
386   }
388   checkResultOfMakingParentNonEditable(aExpectedResult) {
389     this.#checkResult(aExpectedResult);
390   }
393 class IMEStateOutsideContentEditableOnReadonlyChangeTester {
394   static #sFocusTargets = [
395     {
396       tag: "input",
397       type: "text",
398       readonly: false,
399     },
400     {
401       tag: "input",
402       type: "text",
403       readonly: true,
404     },
405     {
406       tag: "textarea",
407       readonly: false,
408     },
409     {
410       tag: "textarea",
411       readonly: true,
412     },
413     {
414       tag: "button",
415     },
416     {
417       tag: "body",
418     },
419   ];
421   static get numberOfFocusTargets() {
422     return IMEStateOutsideContentEditableOnReadonlyChangeTester.#sFocusTargets
423       .length;
424   }
426   static #maybeCreateElement(aDocument, aFocusTarget) {
427     if (aFocusTarget.tag == "body") {
428       return null;
429     }
430     const element = aDocument.createElement(aFocusTarget.tag);
431     if (aFocusTarget.type !== undefined) {
432       element.setAttribute("type", aFocusTarget.type);
433     }
434     if (aFocusTarget.readonly) {
435       element.setAttribute("readonly", "");
436     }
437     return element;
438   }
440   #getDescription() {
441     return `<${this.#mFocusTarget.tag}${
442       this.#mFocusTarget.type !== undefined
443         ? ` type=${this.#mFocusTarget.type}`
444         : ""
445     }${this.#mFocusTarget.readonly ? " readonly" : ""}>`;
446   }
448   #getExpectedIMEState() {
449     return this.#mFocusTarget.readonly ||
450       this.#mFocusTarget.tag == "button" ||
451       this.#mFocusTarget.tag == "body"
452       ? SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED
453       : SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED;
454   }
456   #flushPendingIMENotifications() {
457     return new Promise(resolve =>
458       this.#mWindow.requestAnimationFrame(() =>
459         this.#mWindow.requestAnimationFrame(resolve)
460       )
461     );
462   }
464   // Runner only fields.
465   #mBody;
466   #mEditingHost;
467   #mFocusTarget;
468   #mFocusTargetElement;
469   #mWindow;
471   // Checker only fields.
472   #mWindowUtils;
473   #mTIPWrapper;
475   clear() {
476     this.#mTIPWrapper?.clearFocusBlurNotifications();
477     this.#mTIPWrapper = null;
478   }
480   /**
481    * @param {number} aIndex Index of the test.
482    * @param {Element} aEditingHost The editing host.
483    * @param {Window} aWindow [optional] The DOM window containing aEditingHost.
484    * @returns {object} Expected result of initial state.
485    */
486   async prepareToRun(aIndex, aEditingHost, aWindow = window) {
487     this.#mWindow = aWindow;
488     this.#mEditingHost = aEditingHost;
489     this.#mEditingHost.removeAttribute("contenteditable");
490     this.#mBody = this.#mEditingHost.ownerDocument.body;
491     this.#mBody.ownerDocument.activeElement?.blur();
492     if (this.#mFocusTargetElement != this.#mBody) {
493       this.#mFocusTargetElement?.remove();
494     }
495     await this.#flushPendingIMENotifications();
496     this.#mFocusTarget =
497       IMEStateOutsideContentEditableOnReadonlyChangeTester.#sFocusTargets[
498         aIndex
499       ];
500     this.#mFocusTargetElement =
501       IMEStateOutsideContentEditableOnReadonlyChangeTester.#maybeCreateElement(
502         this.#mBody.ownerDocument,
503         this.#mFocusTarget
504       );
505     if (this.#mFocusTargetElement) {
506       this.#mBody.appendChild(this.#mFocusTargetElement);
507       this.#mFocusTargetElement.focus();
508     }
509     await this.#flushPendingIMENotifications();
510     const expectedIMEState = this.#getExpectedIMEState();
511     return {
512       description: `when ${this.#getDescription()} simply has focus`,
513       expectedIMEState,
514       expectedIMEFocus:
515         expectedIMEState !=
516         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
517     };
518   }
520   #checkResult(aExpectedResult) {
521     const description = "IMEStateOutsideContentEditableOnReadonlyChangeTester";
522     is(
523       this.#mWindowUtils.IMEStatus,
524       aExpectedResult.expectedIMEState,
525       `${description}: IME state should be proper one for the focused element ${aExpectedResult.description}`
526     );
527     is(
528       this.#mTIPWrapper.IMEHasFocus,
529       aExpectedResult.expectedIMEFocus,
530       `${description}: IME should ${
531         aExpectedResult.expectedIMEFocus ? "" : "not "
532       }have focus ${aExpectedResult.description}`
533     );
534   }
536   /**
537    * @param {object} aExpectedResult The expected result returned by prepareToRun().
538    * @param {Window} aWindow The window whose IME state should be checked.
539    * @param {TIPWrapper} aTIPWrapper The TIP wrapper of aWindow.
540    */
541   checkResultOfPreparation(aExpectedResult, aWindow, aTIPWrapper) {
542     this.#mWindowUtils = SpecialPowers.wrap(aWindow).windowUtils;
543     this.#mTIPWrapper = aTIPWrapper;
544     this.#checkResult(aExpectedResult);
545   }
547   async runToMakeParentEditingHost() {
548     this.#mEditingHost.setAttribute("contenteditable", "");
549     await this.#flushPendingIMENotifications();
550     const expectedIMEState = this.#getExpectedIMEState();
551     return {
552       description: `when parent of ${this.#getDescription()} becomes contenteditable`,
553       expectedIMEState,
554       expectedIMEFocus:
555         expectedIMEState !=
556         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
557     };
558   }
560   checkResultOfMakingParentEditingHost(aExpectedResult) {
561     this.#checkResult(aExpectedResult);
562   }
564   async runToMakeHTMLEditorReadonly() {
565     const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
566     editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
567     await this.#flushPendingIMENotifications();
568     const expectedIMEState = this.#getExpectedIMEState();
569     return {
570       description: `when HTMLEditor for parent of ${this.#getDescription()} becomes readonly`,
571       expectedIMEState,
572       expectedIMEFocus:
573         expectedIMEState !=
574         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
575     };
576   }
578   checkResultOfMakingHTMLEditorReadonly(aExpectedResult) {
579     this.#checkResult(aExpectedResult);
580   }
582   async runToMakeHTMLEditorEditable() {
583     const editor = SpecialPowers.wrap(this.#mWindow).docShell.editor;
584     editor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask;
585     await this.#flushPendingIMENotifications();
586     const expectedIMEState = this.#getExpectedIMEState();
587     return {
588       description: `when HTMLEditor for parent of ${this.#getDescription()} becomes editable`,
589       expectedIMEState,
590       expectedIMEFocus:
591         expectedIMEState !=
592         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
593     };
594   }
596   checkResultOfMakingHTMLEditorEditable(aExpectedResult) {
597     this.#checkResult(aExpectedResult);
598   }
600   async runToMakeParentNonEditingHost() {
601     this.#mEditingHost.removeAttribute("contenteditable");
602     await this.#flushPendingIMENotifications();
603     const expectedIMEState = this.#getExpectedIMEState();
604     return {
605       description: `when parent of ${this.#getDescription()} becomes non-editable`,
606       expectedIMEState,
607       expectedIMEFocus:
608         expectedIMEState !=
609         SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED,
610     };
611   }
613   checkResultOfMakingParentNonEditable(aExpectedResult) {
614     this.#checkResult(aExpectedResult);
615   }