Bug 1467796 - part 2: Make autocomplete use new method TextEditor::ReplaceTextAsAction() which replaces all text with specified text r=m_kato

InputEvent.inputType needs to distinguish whether inserting text is caused
by insertText command or replaced by autocomplete or spellchecker.
Therefore, nsTextEditorState::SetValue() cannot use
TextEditor::InsertTextAsAction() nor TextEditor::DeleteSelectionAsAction().

This patch reuses TextEditor::SetText()'s slow path for the new method.

Note that the new method uses EditSubAction::eInsertText as top level edit sub-
action because specifying this improves undo/redo behavior.

And also this patch modifies test_bug1368544.html.  Oddly, only on Android,
we get different result.  After removing all text with setUserInput(""),
TextEditor::DeleteSelectionAsSubAction() removes both text node and non-bogus
<br> element from the anonymous-div element.  However, only on Android, new
<br> element is recreated.  I've not understood where this difference comes
from yet.

MozReview-Commit-ID: GKNksctGik

--HG--
rename : toolkit/content/tests/chrome/file_autocomplete_with_composition.js => toolkit/content/tests/chrome/file_editor_with_autocomplete.js
rename : toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html => toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html
rename : toolkit/content/tests/chrome/test_autocomplete_with_composition_on_textbox.xul => toolkit/content/tests/chrome/test_editor_for_textbox_with_autocomplete.xul
extra : rebase_source : b90419d9e5a01e86f6e6418f8df002c91416acae
This commit is contained in:
Masayuki Nakano 2018-07-03 22:25:52 +09:00
parent efd8891532
commit 08f4c56c7e
9 changed files with 672 additions and 11 deletions

View File

@ -2425,10 +2425,24 @@ nsTextEditorState::SetValue(const nsAString& aValue, const nsAString* aOldValue,
bool notifyValueChanged = !!(aFlags & eSetValue_Notify);
mTextListener->SetValueChanged(notifyValueChanged);
// We preserve the undo history if we are explicitly setting the
// value for the user's input, or if we are setting the value for a
// XUL text control.
if (aFlags & (eSetValue_BySetUserInput | eSetValue_ForXUL)) {
if (aFlags & eSetValue_BySetUserInput) {
// If the caller inserts text as part of user input, for example,
// autocomplete, we need to replace the text as "insert string"
// because undo should cancel only this operation (i.e., previous
// transactions typed by user shouldn't be merged with this).
DebugOnly<nsresult> rv = textEditor->ReplaceTextAsAction(newValue);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to set the new value");
} else if (aFlags & eSetValue_ForXUL) {
// On XUL <textbox> element, we need to preserve existing undo
// transactions.
// XXX Do we really need to do such complicated optimization?
// This was landed for web pages which set <textarea> value
// per line (bug 518122). For example:
// for (;;) {
// textarea.value += oneLineText + "\n";
// }
// However, this path won't be used in web content anymore.
nsCOMPtr<nsISelectionController> kungFuDeathGrip = mSelCon.get();
uint32_t currentLength = currentValue.Length();
uint32_t newlength = newValue.Length();
@ -2457,6 +2471,9 @@ nsTextEditorState::SetValue(const nsAString& aValue, const nsAString* aOldValue,
"Failed to insert the new value");
}
} else {
// On <input> or <textarea>, we shouldn't preserve existing undo
// transactions because other browsers do not preserve them too
// and not preserving transactions makes setting value faster.
AutoDisableUndo disableUndo(textEditor);
if (selection) {
// Since we don't use undo transaction, we don't need to store

View File

@ -905,9 +905,12 @@ TextEditRules::WillSetText(bool* aCancel,
*aHandled = false;
*aCancel = false;
if (!IsPlaintextEditor() || TextEditorRef().IsIMEComposing() ||
if (!IsPlaintextEditor() ||
TextEditorRef().IsIMEComposing() ||
TextEditorRef().IsUndoRedoEnabled() ||
aMaxLength != -1) {
// SetTextImpl only supports plain text editor without IME.
// SetTextImpl only supports plain text editor without IME and
// when we don't need to make it undoable.
return NS_OK;
}

View File

@ -1136,6 +1136,36 @@ TextEditor::SetText(const nsAString& aString)
{
MOZ_ASSERT(aString.FindChar(static_cast<char16_t>('\r')) == kNotFound);
AutoPlaceholderBatch batch(this, nullptr);
nsresult rv = SetTextAsSubAction(aString);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
TextEditor::ReplaceTextAsAction(const nsAString& aString)
{
AutoPlaceholderBatch batch(this, nullptr);
// This should emulates inserting text for better undo/redo behavior.
AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
*this, EditSubAction::eInsertText,
nsIEditor::eNext);
nsresult rv = SetTextAsSubAction(aString);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
TextEditor::SetTextAsSubAction(const nsAString& aString)
{
MOZ_ASSERT(mPlaceholderBatch);
if (NS_WARN_IF(!mRules)) {
return NS_ERROR_NOT_INITIALIZED;
}
@ -1143,8 +1173,6 @@ TextEditor::SetText(const nsAString& aString)
// Protect the edit rules object from dying
RefPtr<TextEditRules> rules(mRules);
// delete placeholder txns merge.
AutoPlaceholderBatch batch(this, nullptr);
AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
*this, EditSubAction::eSetText,
nsIEditor::eNext);
@ -1169,6 +1197,11 @@ TextEditor::SetText(const nsAString& aString)
return NS_OK;
}
if (!handled) {
// Note that do not notify selectionchange caused by selecting all text
// because it's preparation of our delete implementation so web apps
// shouldn't receive such selectionchange before the first mutation.
AutoUpdateViewBatch preventSelectionChangeEvent(this);
// We want to select trailing BR node to remove all nodes to replace all,
// but TextEditor::SelectEntireDocument doesn't select that BR node.
if (rules->DocumentIsEmpty()) {

View File

@ -160,6 +160,14 @@ public:
*/
nsresult SetText(const nsAString& aString);
/**
* Replace all text in this editor with aString and treat the change as
* inserting the string.
*
* @param aString The string to set.
*/
nsresult ReplaceTextAsAction(const nsAString& aString);
/**
* OnInputParagraphSeparator() is called when user tries to separate current
* paragraph with Enter key press or something.
@ -263,6 +271,14 @@ protected: // May be called by friends.
DeleteSelectionWithTransaction(EDirection aAction,
EStripWrappers aStripWrappers);
/**
* Replace existed string with aString. Caller must guarantee that there
* is a placeholder transaction which will have the transaction.
*
* @ param aString The string to be set.
*/
nsresult SetTextAsSubAction(const nsAString& aString);
/**
* InsertBrElementWithTransaction() creates a <br> element and inserts it
* before aPointToInsert. Then, tries to collapse selection at or after the

View File

@ -52,9 +52,20 @@ SimpleTest.waitForFocus(() => {
textarea.value = "ABC";
SpecialPowers.wrap(textarea).setUserInput("");
ok(editor.rootElement.hasChildNodes(),
"editor of textarea has child node even if value is empty");
is(textarea.value, "",
"textarea should become empty when setUserInput() is called with empty string");
if (navigator.appVersion.includes("Android")) {
todo(!editor.rootElement.hasChildNodes(),
"editor of textarea should have no children when user input emulation set the value to empty");
if (editor.rootElement.childNodes.length > 0) {
is(editor.rootElement.childNodes.length, 1, "There should be only one <br> node");
is(editor.rootElement.firstChild.tagName.toLowerCase(), "br", "The node should be a <br> element node");
is(editor.rootElement.firstChild.getAttribute("_moz_editor_bogus_node"), null, "The <br> should not be a bogus node");
}
} else {
ok(!editor.rootElement.hasChildNodes(),
"editor of textarea should have no children when user input emulation set the value to empty");
}
textarea.value = "ABC";
synthesizeKey("KEY_Enter", {repeat: 2});
textarea.value = "";

View File

@ -16,6 +16,7 @@ support-files =
dialog_dialogfocus2.xul
file_about_networking_wsh.py
file_autocomplete_with_composition.js
file_editor_with_autocomplete.js
findbar_entireword_window.xul
findbar_events_window.xul
findbar_window.xul
@ -106,6 +107,8 @@ skip-if = toolkit == "cocoa"
[test_custom_element_base.xul]
[test_deck.xul]
[test_dialogfocus.xul]
[test_editor_for_input_with_autocomplete.html]
[test_editor_for_textbox_with_autocomplete.xul]
[test_findbar.xul]
subsuite = clipboard
[test_findbar_entireword.xul]

View File

@ -0,0 +1,377 @@
// nsDoTestsForEditorWithAutoComplete tests basic functions of editor with autocomplete.
// Users must include SimpleTest.js and EventUtils.js, and register "Mozilla" to the autocomplete for the target.
ChromeUtils.import("resource://gre/modules/Services.jsm");
async function waitForCondition(condition) {
return new Promise(resolve => {
var tries = 0;
var interval = setInterval(function() {
if (condition() || tries >= 60) {
moveOn();
}
tries++;
}, 100);
var moveOn = function() { clearInterval(interval); resolve(); };
});
}
function nsDoTestsForEditorWithAutoComplete(aDescription,
aWindow,
aTarget,
aAutoCompleteController,
aIsFunc,
aGetTargetValueFunc) {
this._description = aDescription;
this._window = aWindow;
this._target = aTarget;
this._controller = aAutoCompleteController;
this._is = aIsFunc;
this._getTargetValue = aGetTargetValueFunc;
this._target.focus();
this._DefaultCompleteDefaultIndex =
this._controller.input.completeDefaultIndex;
}
nsDoTestsForEditorWithAutoComplete.prototype = {
_window: null,
_target: null,
_controller: null,
_DefaultCompleteDefaultIndex: false,
_description: "",
_is: null,
_getTargetValue() { return "not initialized"; },
run: async function runTestsImpl() {
for (let test of this._tests) {
if (this._controller.input.completeDefaultIndex != test.completeDefaultIndex) {
this._controller.input.completeDefaultIndex = test.completeDefaultIndex;
}
if (test.execute(this._window, this._target) === false) {
continue;
}
await waitForCondition(() => {
return this._controller.searchStatus >=
Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
});
this._checkResult(test);
}
this._controller.input.completeDefaultIndex = this._DefaultCompleteDefaultIndex;
},
_checkResult(aTest) {
this._is(this._getTargetValue(), aTest.value,
this._description + ", " + aTest.description + ": value");
this._is(this._controller.searchString, aTest.searchString,
this._description + ", " + aTest.description + ": searchString");
this._is(this._controller.input.popupOpen, aTest.popup,
this._description + ", " + aTest.description + ": popupOpen");
this._is(this._controller.searchStatus, Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH,
this._description + ", " + aTest.description + ": status");
},
_tests: [
{ description: "Undo/Redo behavior check when typed text exactly matches the case: type 'Mo'",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("M", { shiftKey: true }, aWindow);
synthesizeKey("o", {}, aWindow);
return true;
}, popup: true, value: "Mo", searchString: "Mo"
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case: select 'Mozilla' to complete the word",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("KEY_ArrowDown", {}, aWindow);
synthesizeKey("KEY_Enter", {}, aWindow);
return true;
}, popup: false, value: "Mozilla", searchString: "Mozilla"
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case: undo the word, but typed text shouldn't be canceled",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("z", { accelKey: true }, aWindow);
return true;
}, popup: true, value: "Mo", searchString: "Mo"
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case: undo the typed text",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("z", { accelKey: true }, aWindow);
return true;
}, popup: false, value: "", searchString: ""
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case: redo the typed text",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
return true;
}, popup: true, value: "Mo", searchString: "Mo"
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case: redo the word",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
return true;
}, popup: true, value: "Mozilla", searchString: "Mozilla"
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case: removing all text for next test...",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("a", { accelKey: true }, aWindow);
synthesizeKey("KEY_Backspace", {}, aWindow);
return true;
}, popup: false, value: "", searchString: ""
},
{ description: "Undo/Redo behavior check when typed text does not match the case: type 'mo'",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("m", {}, aWindow);
synthesizeKey("o", {}, aWindow);
return true;
}, popup: true, value: "mo", searchString: "mo"
},
{ description: "Undo/Redo behavior check when typed text does not match the case: select 'Mozilla' to complete the word",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("KEY_ArrowDown", {}, aWindow);
synthesizeKey("KEY_Enter", {}, aWindow);
return true;
}, popup: false, value: "Mozilla", searchString: "Mozilla"
},
{ description: "Undo/Redo behavior check when typed text does not match the case: undo the word, but typed text shouldn't be canceled",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("z", { accelKey: true }, aWindow);
return true;
}, popup: true, value: "mo", searchString: "mo"
},
{ description: "Undo/Redo behavior check when typed text does not match the case: undo the typed text",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("z", { accelKey: true }, aWindow);
return true;
}, popup: false, value: "", searchString: ""
},
{ description: "Undo/Redo behavior check when typed text does not match the case: redo the typed text",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
return true;
}, popup: true, value: "mo", searchString: "mo"
},
{ description: "Undo/Redo behavior check when typed text does not match the case: redo the word",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
return true;
}, popup: true, value: "Mozilla", searchString: "Mozilla"
},
{ description: "Undo/Redo behavior check when typed text does not match the case: removing all text for next test...",
completeDefaultIndex: false,
execute(aWindow, aTarget) {
synthesizeKey("a", { accelKey: true }, aWindow);
synthesizeKey("KEY_Backspace", {}, aWindow);
return true;
}, popup: false, value: "", searchString: ""
},
// Testing for nsIAutoCompleteInput.completeDefaultIndex being true.
{ description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): type 'Mo'",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("M", { shiftKey: true }, aWindow);
synthesizeKey("o", {}, aWindow);
return true;
}, popup: true, value: "Mozilla", searchString: "Mo"
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): select 'Mozilla' to complete the word",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("KEY_ArrowDown", {}, aWindow);
synthesizeKey("KEY_Enter", {}, aWindow);
return true;
}, popup: false, value: "Mozilla", searchString: "Mozilla"
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("z", { accelKey: true }, aWindow);
return true;
}, popup: true, value: "Mo", searchString: "Mo"
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the typed text",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("z", { accelKey: true }, aWindow);
return true;
}, popup: false, value: "", searchString: ""
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the typed text",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
return true;
}, popup: true, value: "Mozilla", searchString: "Mo"
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the word",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
return true;
}, popup: true, value: "Mozilla", searchString: "Mo"
},
{ description: "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): removing all text for next test...",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("a", { accelKey: true }, aWindow);
synthesizeKey("KEY_Backspace", {}, aWindow);
return true;
}, popup: false, value: "", searchString: ""
},
{ description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): type 'mo'",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("m", {}, aWindow);
synthesizeKey("o", {}, aWindow);
return true;
}, popup: true, value: "mozilla", searchString: "mo"
},
{ description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): select 'Mozilla' to complete the word",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("KEY_ArrowDown", {}, aWindow);
synthesizeKey("KEY_Enter", {}, aWindow);
return true;
}, popup: false, value: "Mozilla", searchString: "Mozilla"
},
// Different from "exactly matches the case" case, modifying the case causes one additional transaction.
// Although we could make this transaction ignored.
{ description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the selected word, but typed text shouldn't be canceled",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("z", { accelKey: true }, aWindow);
return true;
}, popup: true, value: "mozilla", searchString: "mozilla"
},
{ description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("z", { accelKey: true }, aWindow);
return true;
}, popup: true, value: "mo", searchString: "mo"
},
{ description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the typed text",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("z", { accelKey: true }, aWindow);
return true;
}, popup: false, value: "", searchString: ""
},
// XXX This is odd case. Consistency with undo behavior, this should restore "mo".
// However, looks like that autocomplete automatically restores "mozilla".
// Additionally, looks like that it causes clearing the redo stack.
// Therefore, the following redo operations do nothing.
{ description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the typed text",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
return true;
}, popup: true, value: "mozilla", searchString: "mo"
},
{ description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the default index word",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
return true;
}, popup: true, value: "mozilla", searchString: "mo"
},
{ description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the word",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
return true;
}, popup: true, value: "mozilla", searchString: "mo"
},
{ description: "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): removing all text for next test...",
completeDefaultIndex: true,
execute(aWindow, aTarget) {
// Undo/Redo behavior on XUL <textbox> with completeDefaultIndex is set to true is unstable. Skip it now.
if (aTarget.tagName === "textbox") {
return false;
}
synthesizeKey("a", { accelKey: true }, aWindow);
synthesizeKey("KEY_Backspace", {}, aWindow);
return true;
}, popup: false, value: "", searchString: ""
},
]
};

View File

@ -0,0 +1,75 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Basic editor behavior for HTML input element with autocomplete</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script type="text/javascript" src="file_editor_with_autocomplete.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
</head>
<body>
<p id="display"></p>
<div id="content">
<iframe id="formTarget" name="formTarget"></iframe>
<form action="data:text/html," target="formTarget">
<input name="test" id="input"><input type="submit">
</form>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">
SimpleTest.waitForExplicitFinish();
async function registerWord(aTarget, aAutoCompleteController) {
// Register a word to the form history.
aTarget.focus();
aTarget.value = "Mozilla";
synthesizeKey("KEY_Enter");
await waitForCondition(() => {
if (aAutoCompleteController.searchStatus == aAutoCompleteController.STATUS_NONE ||
aAutoCompleteController.searchStatus == aAutoCompleteController.STATUS_COMPLETE_NO_MATCH) {
aAutoCompleteController.startSearch("Mozilla");
}
return aAutoCompleteController.matchCount > 0;
});
aTarget.value = "";
synthesizeKey("KEY_Escape");
}
async function runTests() {
var formFillController =
SpecialPowers.getFormFillController()
.QueryInterface(Ci.nsIAutoCompleteInput);
var originalFormFillTimeout = formFillController.timeout;
SpecialPowers.attachFormFillControllerTo(window);
var target = document.getElementById("input");
// Register a word to the form history.
await registerWord(target, formFillController.controller);
let tests1 = new nsDoTestsForEditorWithAutoComplete(
"Testing on HTML input (asynchronously search)",
window, target, formFillController.controller, is,
function() { return target.value; });
await tests1.run();
target.setAttribute("timeout", 0);
let tests2 = new nsDoTestsForEditorWithAutoComplete(
"Testing on HTML input (synchronously search)",
window, target, formFillController.controller, is,
function() { return target.value; });
await tests2.run();
formFillController.timeout = originalFormFillTimeout;
SpecialPowers.detachFormFillControllerFrom(window);
SimpleTest.finish();
}
SimpleTest.waitForFocus(runTests);
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,126 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
type="text/css"?>
<window title="Basic editor behavior for XUL textbox element with autocomplete"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
<script type="text/javascript"
src="file_editor_with_autocomplete.js" />
<textbox id="textbox" type="autocomplete"
autocompletesearch="simpleForComposition"/>
<body xmlns="http://www.w3.org/1999/xhtml">
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
<script class="testbody" type="application/javascript">
<![CDATA[
SimpleTest.waitForExplicitFinish();
const nsIAutoCompleteResult = Ci.nsIAutoCompleteResult;
// This result can't be constructed in-line, because otherwise we leak memory.
function nsAutoCompleteSimpleResult(aString)
{
this.searchString = aString;
if (aString == "" ||
aString.toLowerCase() == "mozilla".substr(0, aString.length)) {
this.searchResult = nsIAutoCompleteResult.RESULT_SUCCESS;
this.matchCount = 1;
this._value = "Mozilla";
} else {
this.searchResult = nsIAutoCompleteResult.RESULT_NOMATCH;
this.matchCount = 0;
this._value = "";
}
}
nsAutoCompleteSimpleResult.prototype = {
_value: "",
searchString: null,
searchResult: nsIAutoCompleteResult.RESULT_FAILURE,
defaultIndex: 0,
errorDescription: null,
matchCount: 0,
getValueAt: function(aIndex) { return aIndex == 0 ? this._value : null; },
getCommentAt: function() { return null; },
getStyleAt: function() { return null; },
getImageAt: function() { return null; },
getFinalCompleteValueAt: function(aIndex) { return this.getValueAt(aIndex); },
getLabelAt: function() { return null; },
removeValueAt: function() {}
};
// A basic autocomplete implementation that either returns one result or none
var autoCompleteSimpleID =
Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca");
var autoCompleteSimpleName =
"@mozilla.org/autocomplete/search;1?name=simpleForComposition"
var autoCompleteSimple = {
QueryInterface: function(iid) {
if (iid.equals(Ci.nsISupports) ||
iid.equals(Ci.nsIFactory) ||
iid.equals(Ci.nsIAutoCompleteSearch))
return this;
throw Cr.NS_ERROR_NO_INTERFACE;
},
createInstance: function(outer, iid) {
return this.QueryInterface(iid);
},
startSearch: function(aString, aParam, aResult, aListener) {
var result = new nsAutoCompleteSimpleResult(aString);
aListener.onSearchResult(this, result);
},
stopSearch: function() {}
};
var componentManager =
Components.manager
.QueryInterface(Ci.nsIComponentRegistrar);
componentManager.registerFactory(autoCompleteSimpleID,
"Test Simple Autocomplete for composition",
autoCompleteSimpleName, autoCompleteSimple);
async function runTests()
{
var target = document.getElementById("textbox");
target.setAttribute("timeout", 1);
let tests1 = new nsDoTestsForEditorWithAutoComplete(
"Testing on XUL textbox (asynchronously search)",
window, target, target.controller, is,
function() { return target.value; });
await tests1.run();
target.setAttribute("timeout", 0);
let tests2 = new nsDoTestsForEditorWithAutoComplete(
"Testing on XUL textbox (synchronously search)",
window, target, target.controller, is,
function() { return target.value; });
await tests2.run();
// Unregister the factory so that we don't get in the way of other
// tests
componentManager.unregisterFactory(autoCompleteSimpleID,
autoCompleteSimple);
SimpleTest.finish();
}
SimpleTest.waitForFocus(runTests);
]]>
</script>
</window>