Commit b040050f authored by Olivier Robin's avatar Olivier Robin Committed by Commit Bot

Only send last event on each runloop

- If multiple events are received on the same runloop,
  autofill will only react to the last one. Only send this one to avoid useless computation.
- Also avoid to send events on unfocus events, as autofill UI should only apply to the
  field that has focus (and is the keyboard responder).

Cq-Include-Trybots: master.tryserver.chromium.mac:ios-simulator-cronet;master.tryserver.chromium.mac:ios-simulator-full-configs
Change-Id: Idc316480a4c03550766d549104609fb5b052c38d
Reviewed-on: https://chromium-review.googlesource.com/998154Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Reviewed-by: default avatarVadym Doroshenko <dvadym@chromium.org>
Reviewed-by: default avatarMoe Ahmadi <mahmadi@chromium.org>
Commit-Queue: Olivier Robin <olivierrobin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#551616}
parent f65fe25c
......@@ -30,18 +30,42 @@ class FormJsTest : public web::WebJsTest<web::WebTestWithWebState> {
std::make_unique<FormTestClient>()) {}
};
// Tests that keyup event correctly delivered to WebStateObserver.
TEST_F(FormJsTest, KeyUpEvent) {
// Tests that keyup event correctly delivered to WebStateObserver if the element
// is focused.
TEST_F(FormJsTest, KeyUpEventFocused) {
web::TestWebStateObserver observer(web_state());
LoadHtml(@"<p></p>");
LoadHtml(@"<p><input id='test'/></p>");
ASSERT_FALSE(observer.form_activity_info());
ExecuteJavaScript(@"document.dispatchEvent(new KeyboardEvent('keyup'));");
ExecuteJavaScript(
@"var e = document.getElementById('test');"
"e.focus();"
"var ev = new KeyboardEvent('keyup', {bubbles:true});"
"e.dispatchEvent(ev);");
web::TestWebStateObserver* block_observer = &observer;
WaitForCondition(^bool {
return block_observer->form_activity_info() != nullptr;
});
web::TestFormActivityInfo* info = observer.form_activity_info();
ASSERT_TRUE(info);
EXPECT_EQ("keyup", info->form_activity.type);
EXPECT_FALSE(info->form_activity.input_missing);
}
// Tests that keyup event is not delivered to WebStateObserver if the element is
// not focused.
TEST_F(FormJsTest, KeyUpEventNotFocused) {
web::TestWebStateObserver observer(web_state());
LoadHtml(@"<p><input id='test'/></p>");
ASSERT_FALSE(observer.form_activity_info());
ExecuteJavaScript(
@"var e = document.getElementById('test');"
"var ev = new KeyboardEvent('keyup', {bubbles:true});"
"e.dispatchEvent(ev);");
WaitForBackgroundTasks();
web::TestFormActivityInfo* info = observer.form_activity_info();
ASSERT_FALSE(info);
}
// Tests that focus event correctly delivered to WebStateObserver.
TEST_F(FormJsTest, FocusMainFrame) {
web::TestWebStateObserver observer(web_state());
......@@ -52,6 +76,10 @@ TEST_F(FormJsTest, FocusMainFrame) {
"</form>");
ASSERT_FALSE(observer.form_activity_info());
ExecuteJavaScript(@"document.getElementById('id1').focus();");
web::TestWebStateObserver* block_observer = &observer;
WaitForCondition(^bool {
return block_observer->form_activity_info() != nullptr;
});
web::TestFormActivityInfo* info = observer.form_activity_info();
ASSERT_TRUE(info);
EXPECT_EQ("focus", info->form_activity.type);
......@@ -88,6 +116,10 @@ TEST_F(FormJsTest, FocusSameOriginIFrame) {
ExecuteJavaScript(
@"document.getElementById('frame1').contentDocument.getElementById('id1')"
@".focus()");
web::TestWebStateObserver* block_observer = &observer;
WaitForCondition(^bool {
return block_observer->form_activity_info() != nullptr;
});
web::TestFormActivityInfo* info = observer.form_activity_info();
ASSERT_TRUE(info);
EXPECT_EQ("focus", info->form_activity.type);
......
......@@ -58,6 +58,16 @@ __gCrWeb.form.formMutationObserver = null;
*/
__gCrWeb.form.formMutationMessageToSend = null;
/**
* A message scheduled to be sent to host on the next runloop.
*/
__gCrWeb.form.messageToSend = null;
/**
* The last HTML element that was focused by the user.
*/
__gCrWeb.form.lastFocusedElement = null;
/**
* Based on Element::isFormControlElement() (WebKit)
* @param {Element} element A DOM element.
......@@ -287,31 +297,55 @@ __gCrWeb.form.getFormElementFromIdentifier = function(name) {
return null;
};
/**
* Schedule |mesg| to be sent on next runloop.
* If called multiple times on the same runloop, only the last message is really
* sent.
*/
var sendMessageOnNextLoop_ = function(mesg) {
if (!__gCrWeb.form.messageToSend) {
setTimeout(function() {
__gCrWeb.message.invokeOnHost(__gCrWeb.form.messageToSend);
__gCrWeb.form.messageToSend = null;
}, 0);
}
__gCrWeb.form.messageToSend = mesg;
}
/**
* Focus and input events for form elements are messaged to the main
* application for broadcast to WebStateObservers.
* Focus, input, change, keyup and blur events for form elements (form and input
* elements) are messaged to the main application for broadcast to
* WebStateObservers.
* Events will be included in a message to be sent in a future runloop (without
* delay). If an event is already scheduled to be sent, it is replaced by |evt|.
* Notably, 'blur' event will not be transmitted to the main application if they
* are triggered by the focus of another element as the 'focus' event will
* replace it.
* Only the events targetting the active element (or the previously active in
* case of 'blur') are sent to the main application.
* This is done with a single event handler for each type being added to the
* main document element which checks the source element of the event; this
* is much easier to manage than adding handlers to individual elements.
* @private
*/
var formActivity_ = function(evt) {
var srcElement = evt.srcElement;
var value = srcElement.value || '';
var fieldType = srcElement.type || '';
var target = evt.target;
var value = target.value || '';
var fieldType = target.type || '';
if (evt.type != 'blur') {
__gCrWeb.form.lastFocusedElement = document.activeElement;
}
if (target != __gCrWeb.form.lastFocusedElement) return;
var msg = {
'command': 'form.activity',
'formName': __gCrWeb.form.getFormIdentifier(evt.srcElement.form),
'fieldName': __gCrWeb.form.getFieldName(srcElement),
'fieldIdentifier': __gCrWeb.form.getFieldIdentifier(srcElement),
'formName': __gCrWeb.form.getFormIdentifier(evt.target.form),
'fieldName': __gCrWeb.form.getFieldName(target),
'fieldIdentifier': __gCrWeb.form.getFieldIdentifier(target),
'fieldType': fieldType,
'type': evt.type,
'value': value
};
__gCrWeb.message.invokeOnHost(msg);
sendMessageOnNextLoop_(msg);
};
/**
......
......@@ -168,7 +168,9 @@ class AutofillControllerTest : public ChromeWebTest {
void SetUpKeyValueData();
// Blocks until suggestion retrieval has completed.
void WaitForSuggestionRetrieval();
// If |wait_for_trigger| is yes, wait for the call to
// |retrieveSuggestionsForForm| to avoid considering a former call.
void WaitForSuggestionRetrieval(BOOL wait_for_trigger);
// Fails if the specified metric was not registered the given number of times.
void ExpectMetric(const std::string& histogram_name, int sum);
......@@ -234,12 +236,15 @@ void AutofillControllerTest::TearDown() {
ChromeWebTest::TearDown();
}
void AutofillControllerTest::WaitForSuggestionRetrieval() {
void AutofillControllerTest::WaitForSuggestionRetrieval(BOOL wait_for_trigger) {
// Wait for the message queue to ensure that JS events fired in the tests
// trigger TestSuggestionController's retrieveSuggestionsForFormNamed: method
// and set suggestionRetrievalComplete to NO.
WaitForBackgroundTasks();
if (wait_for_trigger) {
WaitForCondition(^bool {
return ![suggestion_controller() suggestionRetrievalComplete];
});
}
// Now we can wait for suggestionRetrievalComplete to be set to YES.
WaitForCondition(^bool {
return [suggestion_controller() suggestionRetrievalComplete];
......@@ -352,7 +357,7 @@ TEST_F(AutofillControllerTest, ProfileSuggestions) {
SetUpForSuggestions(kProfileFormHtml);
ui::test::uiview_utils::ForceViewRendering(web_state()->GetView());
ExecuteJavaScript(@"document.forms[0].name.focus()");
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
ExpectMetric("Autofill.AddressSuggestionsCount", 1);
ExpectHappinessMetric(AutofillMetrics::SUGGESTIONS_SHOWN);
EXPECT_EQ(1U, [suggestion_controller() suggestions].count);
......@@ -367,7 +372,7 @@ TEST_F(AutofillControllerTest, ProfileSuggestionsTwoAnonymousForms) {
[NSString stringWithFormat:@"%@%@", kProfileFormHtml, kProfileFormHtml]);
ui::test::uiview_utils::ForceViewRendering(web_state()->GetView());
ExecuteJavaScript(@"document.forms[0].name.focus()");
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
ExpectMetric("Autofill.AddressSuggestionsCount", 1);
ExpectHappinessMetric(AutofillMetrics::SUGGESTIONS_SHOWN);
EXPECT_EQ(1U, [suggestion_controller() suggestions].count);
......@@ -382,7 +387,7 @@ TEST_F(AutofillControllerTest, ProfileSuggestionsFromSelectField) {
SetUpForSuggestions(kProfileFormHtml);
ui::test::uiview_utils::ForceViewRendering(web_state()->GetView());
ExecuteJavaScript(@"document.forms[0].state.focus()");
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
ExpectMetric("Autofill.AddressSuggestionsCount", 1);
ExpectHappinessMetric(AutofillMetrics::SUGGESTIONS_SHOWN);
EXPECT_EQ(1U, [suggestion_controller() suggestions].count);
......@@ -419,7 +424,7 @@ TEST_F(AutofillControllerTest, MultipleProfileSuggestions) {
WaitForBackgroundTasks();
ui::test::uiview_utils::ForceViewRendering(web_state()->GetView());
ExecuteJavaScript(@"document.forms[0].name.focus()");
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
ExpectMetric("Autofill.AddressSuggestionsCount", 2);
ExpectHappinessMetric(AutofillMetrics::SUGGESTIONS_SHOWN);
EXPECT_EQ(2U, [suggestion_controller() suggestions].count);
......@@ -483,7 +488,7 @@ TEST_F(AutofillControllerTest, KeyValueSuggestions) {
// Focus element.
ExecuteJavaScript(@"document.forms[0].greeting.value='B'");
ExecuteJavaScript(@"document.forms[0].greeting.focus()");
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
EXPECT_EQ(1U, [suggestion_controller() suggestions].count);
FormSuggestion* suggestion = [suggestion_controller() suggestions][0];
EXPECT_NSEQ(@"Bonjour", suggestion.value);
......@@ -494,12 +499,13 @@ TEST_F(AutofillControllerTest, KeyValueSuggestions) {
// happen in practice and should not result in a crash or incorrect behavior.
TEST_F(AutofillControllerTest, KeyValueTypedSuggestions) {
SetUpKeyValueData();
ExecuteJavaScript(@"document.forms[0].greeting.select()");
ExecuteJavaScript(@"document.forms[0].greeting.focus()");
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
ExecuteJavaScript(@"event = document.createEvent('TextEvent');");
ExecuteJavaScript(
@"event.initTextEvent('textInput', true, true, window, 'B');");
ExecuteJavaScript(@"document.forms[0].greeting.dispatchEvent(event);");
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
EXPECT_EQ(1U, [suggestion_controller() suggestions].count);
FormSuggestion* suggestion = [suggestion_controller() suggestions][0];
EXPECT_NSEQ(@"Bonjour", suggestion.value);
......@@ -512,7 +518,7 @@ TEST_F(AutofillControllerTest, KeyValueFocusChange) {
// Focus the dummy field and confirm no suggestions are presented.
ExecuteJavaScript(@"document.forms[0].dummy.focus()");
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
EXPECT_EQ(0U, [suggestion_controller() suggestions].count);
// Enter 'B' in the dummy field and confirm no suggestions are presented.
......@@ -520,17 +526,18 @@ TEST_F(AutofillControllerTest, KeyValueFocusChange) {
ExecuteJavaScript(
@"event.initTextEvent('textInput', true, true, window, 'B');");
ExecuteJavaScript(@"document.forms[0].dummy.dispatchEvent(event);");
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
EXPECT_EQ(0U, [suggestion_controller() suggestions].count);
// Enter 'B' in the greeting field and confirm that one suggestion ("Bonjour")
// is presented.
ExecuteJavaScript(@"document.forms[0].greeting.select()");
ExecuteJavaScript(@"document.forms[0].greeting.focus()");
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
ExecuteJavaScript(@"event = document.createEvent('TextEvent');");
ExecuteJavaScript(
@"event.initTextEvent('textInput', true, true, window, 'B');");
ExecuteJavaScript(@"document.forms[0].greeting.dispatchEvent(event);");
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
EXPECT_EQ(1U, [suggestion_controller() suggestions].count);
FormSuggestion* suggestion = [suggestion_controller() suggestions][0];
EXPECT_NSEQ(@"Bonjour", suggestion.value);
......@@ -544,7 +551,7 @@ TEST_F(AutofillControllerTest, NoKeyValueSuggestionsWithoutTyping) {
// Focus element.
ExecuteJavaScript(@"document.forms[0].greeting.focus()");
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES);
EXPECT_EQ(0U, [suggestion_controller() suggestions].count);
}
......@@ -602,7 +609,7 @@ TEST_F(AutofillControllerTest, CreditCardImport) {
// entry with the "credit card interaction" bit set to true.
TEST_F(AutofillControllerTest, HttpCreditCard) {
LoadHtml(kCreditCardAutofocusFormHtml, GURL("http://chromium.test"));
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/NO);
web::SSLStatus ssl_status =
web_state()->GetNavigationManager()->GetLastCommittedItem()->GetSSL();
......@@ -617,7 +624,7 @@ TEST_F(AutofillControllerTest, HttpCreditCard) {
// navigation entry with the "credit card interaction" bit set to true.
TEST_F(AutofillControllerTest, HttpNoCreditCard) {
LoadHtml(kNoCreditCardFormHtml, GURL("http://chromium.test"));
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/NO);
web::SSLStatus ssl_status =
web_state()->GetNavigationManager()->GetLastCommittedItem()->GetSSL();
......@@ -632,7 +639,7 @@ TEST_F(AutofillControllerTest, HttpNoCreditCard) {
// navigation entry with the "credit card interaction" bit set to true.
TEST_F(AutofillControllerTest, HttpsCreditCard) {
LoadHtml(kCreditCardAutofocusFormHtml, GURL("https://chromium.test"));
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/NO);
web::SSLStatus ssl_status =
web_state()->GetNavigationManager()->GetLastCommittedItem()->GetSSL();
......@@ -647,7 +654,7 @@ TEST_F(AutofillControllerTest, HttpsCreditCard) {
// navigation entry with the "credit card interaction" bit set to true.
TEST_F(AutofillControllerTest, HttpsNoCreditCard) {
LoadHtml(kNoCreditCardFormHtml, GURL("https://chromium.test"));
WaitForSuggestionRetrieval();
WaitForSuggestionRetrieval(/*wait_for_trigger=*/NO);
web::SSLStatus ssl_status =
web_state()->GetNavigationManager()->GetLastCommittedItem()->GetSSL();
......
......@@ -957,8 +957,7 @@ TEST_F(PasswordControllerTest, SuggestionUpdateTests) {
{
"Should show all suggestions when focusing empty username field",
@[(@"var evt = document.createEvent('Events');"
"evt.initEvent('focus', true, true, window, 1);"
"username_.dispatchEvent(evt);"),
"username_.focus();"),
@""],
@[@"user0 ••••••••", @"abc ••••••••", showAll],
@"[]=, onkeyup=false, onchange=false"
......@@ -966,8 +965,7 @@ TEST_F(PasswordControllerTest, SuggestionUpdateTests) {
{
"Should show password suggestions when focusing password field",
@[(@"var evt = document.createEvent('Events');"
"evt.initEvent('focus', true, true, window, 1);"
"password_.dispatchEvent(evt);"),
"password_.focus();"),
@""],
@[@"user0 ••••••••", @"abc ••••••••", showAll],
@"[]=, onkeyup=false, onchange=false"
......@@ -975,9 +973,7 @@ TEST_F(PasswordControllerTest, SuggestionUpdateTests) {
{
"Should not filter suggestions when focusing username field with input",
@[(@"username_.value='ab';"
"var evt = document.createEvent('Events');"
"evt.initEvent('focus', true, true, window, 1);"
"username_.dispatchEvent(evt);"),
"username_.focus();"),
@""],
@[@"user0 ••••••••", @"abc ••••••••", showAll],
@"ab[]=, onkeyup=false, onchange=false"
......@@ -1001,6 +997,10 @@ TEST_F(PasswordControllerTest, SuggestionUpdateTests) {
// Pump the run loop so that the host can respond.
WaitForBackgroundTasks();
}
// Wait until suggestions are received.
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^() {
return [GetSuggestionValues() count] > 0;
}));
EXPECT_NSEQ(data.expected_suggestions, GetSuggestionValues());
EXPECT_NSEQ(data.expected_result,
......
......@@ -107,15 +107,12 @@ TEST_F(WebViewAutofillTest, TestDelegateCallbacks) {
fieldIdentifier:kTestFieldID
formName:kTestFormName
value:kTestFieldValue];
NSString* focus_script =
[NSString stringWithFormat:
@"var event = new Event('focus');"
"document.getElementById('%@').dispatchEvent(event);",
kTestFieldID];
NSString* focus_script = [NSString
stringWithFormat:@"document.getElementById('%@').focus();", kTestFieldID];
NSError* focus_error = nil;
test::EvaluateJavaScript(web_view_, focus_script, &focus_error);
ASSERT_NSEQ(nil, focus_error);
[delegate verify];
[delegate verifyWithDelay:kWaitForActionTimeout];
[[delegate expect] autofillController:autofill_controller_
didBlurOnFieldWithName:kTestFieldName
......@@ -124,13 +121,13 @@ TEST_F(WebViewAutofillTest, TestDelegateCallbacks) {
value:kTestFieldValue];
NSString* blur_script =
[NSString stringWithFormat:
@"var event = new Event('blur');"
@"var event = new Event('blur', {bubbles:true});"
"document.getElementById('%@').dispatchEvent(event);",
kTestFieldID];
NSError* blur_error = nil;
test::EvaluateJavaScript(web_view_, blur_script, &blur_error);
ASSERT_NSEQ(nil, blur_error);
[delegate verify];
[delegate verifyWithDelay:kWaitForActionTimeout];
[[delegate expect] autofillController:autofill_controller_
didInputInFieldWithName:kTestFieldName
......@@ -147,7 +144,7 @@ TEST_F(WebViewAutofillTest, TestDelegateCallbacks) {
NSError* input_error = nil;
test::EvaluateJavaScript(web_view_, input_script, &input_error);
ASSERT_NSEQ(nil, input_error);
[delegate verify];
[delegate verifyWithDelay:kWaitForActionTimeout];
[[delegate expect] autofillController:autofill_controller_
didSubmitFormWithName:kTestFormName
......@@ -163,7 +160,7 @@ TEST_F(WebViewAutofillTest, TestDelegateCallbacks) {
NSError* submit_error = nil;
test::EvaluateJavaScript(web_view_, submit_script, &submit_error);
ASSERT_NSEQ(nil, submit_error);
[delegate verify];
[delegate verifyWithDelay:kWaitForActionTimeout];
}
// Tests that CWVAutofillController can fetch, fill, and clear suggestions.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment