chromium/chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl_unittest.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/autofill/autofill_keyboard_accessory_controller_impl.h"

#include <string>

#include "base/strings/utf_string_conversions.h"
#include "base/test/gmock_callback_support.h"
#include "base/time/time.h"
#include "chrome/browser/ui/autofill/autofill_suggestion_controller_test_base.h"
#include "chrome/browser/ui/autofill/test_autofill_keyboard_accessory_controller_autofill_client.h"
#include "components/autofill/core/browser/address_data_manager.h"
#include "components/autofill/core/browser/ui/suggestion_hiding_reason.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/strings/grit/components_strings.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"

namespace autofill {

namespace {

using ::testing::_;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::InSequence;
using ::testing::Mock;
using ::testing::MockFunction;
using ::testing::Return;

std::vector<Suggestion> CreateSuggestionsWithUndoOrClearEntry(
    size_t clear_form_offset) {
  auto create_pw_suggestion = [](std::string_view password,
                                 std::string_view username,
                                 std::string_view origin) {
    Suggestion s(/*main_text=*/username, /*label=*/password,
                 Suggestion::Icon::kNoIcon, SuggestionType::kPasswordEntry);
    s.additional_label = base::UTF8ToUTF16(origin);
    return s;
  };
  std::vector<Suggestion> suggestions = {
      create_pw_suggestion("****************", "Alf", ""),
      create_pw_suggestion("****************", "Berta", "psl.origin.eg"),
      create_pw_suggestion("***", "Carl", "")};
  suggestions.emplace(suggestions.begin() + clear_form_offset, "Clear", "",
                      Suggestion::Icon::kNoIcon, SuggestionType::kUndoOrClear);
  return suggestions;
}

class AutofillKeyboardAccessoryControllerImplTest
    : public AutofillSuggestionControllerTestBase<
          TestAutofillKeyboardAccessoryControllerAutofillClient<>> {
 protected:
  AutofillProfile ShowAutofillProfileSuggestion() {
    AutofillProfile complete_profile = test::GetFullProfile();
    personal_data().address_data_manager().AddProfile(complete_profile);
    ShowSuggestions(manager(), {test::CreateAutofillSuggestion(
                                   SuggestionType::kAddressEntry,
                                   u"Complete autofill profile",
                                   Suggestion::Guid(complete_profile.guid()))});
    return complete_profile;
  }

  CreditCard ShowLocalCardSuggestion() {
    CreditCard local_card = test::GetCreditCard();
    personal_data().payments_data_manager().AddCreditCard(local_card);
    ShowSuggestions(manager(),
                    {test::CreateAutofillSuggestion(
                        SuggestionType::kCreditCardEntry, u"Local credit card",
                        Suggestion::Guid(local_card.guid()))});
    return local_card;
  }
};

}  // namespace

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       AcceptSuggestionRespectsTimeout) {
  // Calls before the threshold are ignored.
  MockFunction<void()> check;
  {
    InSequence s;
    EXPECT_CALL(check, Call);
    EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion);
  }

  ShowSuggestions(manager(), {SuggestionType::kAddressEntry});
  client().popup_controller(manager()).AcceptSuggestion(0);
  task_environment()->FastForwardBy(base::Milliseconds(100));
  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0);
  task_environment()->FastForwardBy(base::Milliseconds(400));

  // Only now suggestions should be accepted.
  check.Call();
  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0);
}

// Tests that reshowing the suggestions resets the accept threshold.
TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       AcceptSuggestionTimeoutIsUpdatedOnUiUpdate) {
  // Calls before the threshold are ignored.
  MockFunction<void()> check;
  {
    InSequence s;
    EXPECT_CALL(check, Call);
    EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion);
  }

  ShowSuggestions(manager(), {SuggestionType::kAddressEntry});
  // Calls before the threshold are ignored.
  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0);
  task_environment()->FastForwardBy(base::Milliseconds(100));
  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0);
  task_environment()->FastForwardBy(base::Milliseconds(400));

  // Show the suggestions again (simulating, e.g., a click somewhere slightly
  // different).
  ShowSuggestions(manager(), {SuggestionType::kAddressEntry});
  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0);

  // After waiting again, suggestions become acceptable.
  task_environment()->FastForwardBy(base::Milliseconds(500));
  check.Call();
  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0);
}

// Tests that calling `Show()` on the controller shows the view.
TEST_F(AutofillKeyboardAccessoryControllerImplTest, ShowCallsView) {
  // Ensure that controller and view have been created.
  client().popup_controller(manager());

  EXPECT_CALL(*client().popup_view(), Show());
  ShowSuggestions(manager(), {Suggestion(u"Autocomplete entry",
                                         SuggestionType::kAutocompleteEntry)});
}

// Tests that calling `Hide()` on the controller hides and destroys the view.
TEST_F(AutofillKeyboardAccessoryControllerImplTest, HideDestroysView) {
  ShowSuggestions(manager(), {Suggestion(u"Autocomplete entry",
                                         SuggestionType::kAutocompleteEntry)});

  EXPECT_CALL(*client().popup_view(), Hide);
  client().popup_controller(manager()).Hide(SuggestionHidingReason::kTabGone);
  // The keyboard accessory view is destroyed synchronously.
  EXPECT_FALSE(client().popup_view());
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       GetRemovalConfirmationText_UnrelatedSuggestionType) {
  std::u16string title;
  std::u16string body;
  ShowSuggestions(
      manager(),
      {Suggestion(u"Entry", SuggestionType::kAddressFieldByFieldFilling)});

  EXPECT_FALSE(client().popup_controller(manager()).GetRemovalConfirmationText(
      0, &title, &body));
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       GetRemovalConfirmationText_InvalidUniqueId) {
  std::u16string title;
  std::u16string body;
  ShowSuggestions(manager(), {test::CreateAutofillSuggestion(
                                 SuggestionType::kAddressFieldByFieldFilling,
                                 u"Entry", Suggestion::Guid("1111"))});

  EXPECT_FALSE(client().popup_controller(manager()).GetRemovalConfirmationText(
      0, &title, &body));
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       GetRemovalConfirmationText_Autocomplete) {
  std::u16string title;
  std::u16string body;
  ShowSuggestions(manager(), {Suggestion(u"Autocomplete entry",
                                         SuggestionType::kAutocompleteEntry)});

  EXPECT_TRUE(client().popup_controller(manager()).GetRemovalConfirmationText(
      0, &title, &body));
  EXPECT_EQ(title, u"Autocomplete entry");
  EXPECT_EQ(body,
            l10n_util::GetStringUTF16(
                IDS_AUTOFILL_DELETE_AUTOCOMPLETE_SUGGESTION_CONFIRMATION_BODY));
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       GetRemovalConfirmationText_LocalCreditCard) {
  CreditCard local_card = ShowLocalCardSuggestion();

  std::u16string title;
  std::u16string body;
  EXPECT_TRUE(client().popup_controller(manager()).GetRemovalConfirmationText(
      0, &title, &body));
  EXPECT_EQ(title, local_card.CardNameAndLastFourDigits());
  EXPECT_EQ(body,
            l10n_util::GetStringUTF16(
                IDS_AUTOFILL_DELETE_CREDIT_CARD_SUGGESTION_CONFIRMATION_BODY));
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       GetRemovalConfirmationText_ServerCreditCard) {
  CreditCard server_card = test::GetMaskedServerCard();
  personal_data().test_payments_data_manager().AddServerCreditCard(server_card);

  std::u16string title;
  std::u16string body;
  ShowSuggestions(manager(),
                  {test::CreateAutofillSuggestion(
                      SuggestionType::kCreditCardEntry, u"Server credit card",
                      Suggestion::Guid(server_card.guid()))});

  EXPECT_FALSE(client().popup_controller(manager()).GetRemovalConfirmationText(
      0, &title, &body));
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       GetRemovalConfirmationText_CompleteAutofillProfile) {
  AutofillProfile complete_profile = ShowAutofillProfileSuggestion();

  std::u16string title;
  std::u16string body;
  EXPECT_TRUE(client().popup_controller(manager()).GetRemovalConfirmationText(
      0, &title, &body));
  EXPECT_EQ(title, complete_profile.GetRawInfo(ADDRESS_HOME_CITY));
  EXPECT_EQ(body,
            l10n_util::GetStringUTF16(
                IDS_AUTOFILL_DELETE_PROFILE_SUGGESTION_CONFIRMATION_BODY));
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       GetRemovalConfirmationText_AutofillProfile_EmptyCity) {
  AutofillProfile profile = test::GetFullProfile();
  profile.ClearFields({ADDRESS_HOME_CITY});
  personal_data().address_data_manager().AddProfile(profile);

  std::u16string title;
  std::u16string body;
  ShowSuggestions(manager(), {test::CreateAutofillSuggestion(
                                 SuggestionType::kAddressEntry,
                                 u"Autofill profile without city",
                                 Suggestion::Guid(profile.guid()))});

  EXPECT_TRUE(client().popup_controller(manager()).GetRemovalConfirmationText(
      0, &title, &body));
  EXPECT_EQ(title, u"Autofill profile without city");
  EXPECT_EQ(body,
            l10n_util::GetStringUTF16(
                IDS_AUTOFILL_DELETE_PROFILE_SUGGESTION_CONFIRMATION_BODY));
}

// Tests that a call to `RemoveSuggestion()` leads to a deletion confirmation
// dialog and, on accepting that dialog, to the deletion of the suggestion and
// the a11y announcement that it was deleted.
TEST_F(AutofillKeyboardAccessoryControllerImplTest, RemoveAfterConfirmation) {
  const auto suggestion =
      Suggestion(u"Autocomplete entry", SuggestionType::kAutocompleteEntry);
  ShowSuggestions(manager(), {suggestion});
  ASSERT_TRUE(client().popup_view());

  EXPECT_CALL(*client().popup_view(), ConfirmDeletion)
      .WillOnce(base::test::RunOnceCallback<2>(/*confirmed=*/true));
  EXPECT_CALL(manager().external_delegate(), RemoveSuggestion(suggestion))
      .WillOnce(Return(true));
  EXPECT_CALL(*client().popup_view(),
              AxAnnounce(Eq(u"Entry Autocomplete entry has been deleted")));

  EXPECT_TRUE(client().popup_controller(manager()).RemoveSuggestion(
      /*index=*/0,
      AutofillMetrics::SingleEntryRemovalMethod::kKeyboardAccessory));
}

// Tests that the correct metrics are logged when the confirmation dialog for
// deleting an Autofill profile is cancelled.
TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       MetricsAfterAddressDeletionDeclined) {
  ShowAutofillProfileSuggestion();
  ASSERT_TRUE(client().popup_view());

  base::HistogramTester histogram;
  EXPECT_CALL(*client().popup_view(), ConfirmDeletion)
      .WillOnce(base::test::RunOnceCallback<2>(/*confirmed=*/false));
  EXPECT_CALL(manager().external_delegate(), RemoveSuggestion).Times(0);

  EXPECT_TRUE(client().popup_controller(manager()).RemoveSuggestion(
      /*index=*/0,
      AutofillMetrics::SingleEntryRemovalMethod::kKeyboardAccessory));
  histogram.ExpectUniqueSample("Autofill.ProfileDeleted.ExtendedMenu", false,
                               1);
  histogram.ExpectUniqueSample("Autofill.ProfileDeleted.Any", false, 1);
}

// Tests that no metrics are logged when the confirmation dialog for deleting a
// credit card is cancelled.
TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       MetricsAfterCreditCardDeletionDeclined) {
  ShowLocalCardSuggestion();
  ASSERT_TRUE(client().popup_view());

  base::HistogramTester histogram;
  EXPECT_CALL(*client().popup_view(), ConfirmDeletion)
      .WillOnce(base::test::RunOnceCallback<2>(/*confirmed=*/false));
  EXPECT_CALL(manager().external_delegate(), RemoveSuggestion).Times(0);

  EXPECT_TRUE(client().popup_controller(manager()).RemoveSuggestion(
      /*index=*/0,
      AutofillMetrics::SingleEntryRemovalMethod::kKeyboardAccessory));
  histogram.ExpectUniqueSample("Autofill.ProfileDeleted.ExtendedMenu", false,
                               0);
  histogram.ExpectUniqueSample("Autofill.ProfileDeleted.Any", false, 0);
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       AcceptPwdSuggestionInvokesWarningAndroid) {
  base::test::ScopedFeatureList scoped_feature_list(
      password_manager::features::
          kUnifiedPasswordManagerLocalPasswordsMigrationWarning);
  ShowSuggestions(manager(), {SuggestionType::kPasswordEntry});

  // Calls are accepted immediately.
  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(1);
  EXPECT_CALL(client().show_pwd_migration_warning_callback(),
              Run(_, _,
                  password_manager::metrics_util::
                      PasswordMigrationWarningTriggers::kKeyboardAcessoryBar));
  task_environment()->FastForwardBy(base::Milliseconds(500));
  client().popup_controller(manager()).AcceptSuggestion(0);
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       AcceptUsernameSuggestionInvokesWarningAndroid) {
  base::test::ScopedFeatureList scoped_feature_list(
      password_manager::features::
          kUnifiedPasswordManagerLocalPasswordsMigrationWarning);
  ShowSuggestions(manager(), {SuggestionType::kPasswordEntry});

  // Calls are accepted immediately.
  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(1);
  EXPECT_CALL(client().show_pwd_migration_warning_callback(), Run);
  task_environment()->FastForwardBy(base::Milliseconds(500));
  client().popup_controller(manager()).AcceptSuggestion(0);
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       AcceptPwdSuggestionNoWarningIfDisabledAndroid) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndDisableFeature(
      password_manager::features::
          kUnifiedPasswordManagerLocalPasswordsMigrationWarning);
  ShowSuggestions(manager(), {SuggestionType::kPasswordEntry});

  // Calls are accepted immediately.
  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(1);
  EXPECT_CALL(client().show_pwd_migration_warning_callback(), Run).Times(0);
  task_environment()->FastForwardBy(base::Milliseconds(500));
  client().popup_controller(manager()).AcceptSuggestion(0);
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       AcceptAddressNoPwdWarningAndroid) {
  base::test::ScopedFeatureList scoped_feature_list(
      password_manager::features::
          kUnifiedPasswordManagerLocalPasswordsMigrationWarning);
  ShowSuggestions(manager(), {SuggestionType::kAddressEntry});

  // Calls are accepted immediately.
  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(1);
  EXPECT_CALL(client().show_pwd_migration_warning_callback(), Run).Times(0);
  task_environment()->FastForwardBy(base::Milliseconds(500));
  client().popup_controller(manager()).AcceptSuggestion(0);
}

// When a suggestion is accepted, the popup is hidden inside
// `delegate->DidAcceptSuggestion()`. On Android, some code is still being
// executed after hiding. This test makes sure no use-after-free, null pointer
// dereferencing or other memory violations occur.
TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       AcceptSuggestionIsMemorySafe) {
  ShowSuggestions(manager(), {SuggestionType::kPasswordEntry});
  task_environment()->FastForwardBy(base::Milliseconds(500));

  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion)
      .WillOnce([this]() {
        client().popup_controller(manager()).Hide(
            SuggestionHidingReason::kAcceptSuggestion);
      });
  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0);
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       DoesNotAcceptUnacceptableSuggestions) {
  Suggestion suggestion(u"Open the pod bay doors, HAL");
  suggestion.is_acceptable = false;
  ShowSuggestions(manager(), {std::move(suggestion)});
  task_environment()->FastForwardBy(base::Milliseconds(500));

  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(0);
  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0);
}

// Tests that the `KeyboardAccessoryController` moves "clear form" suggestions
// to the front.
TEST_F(AutofillKeyboardAccessoryControllerImplTest, ReorderUpdatedSuggestions) {
  const std::vector<Suggestion> suggestions =
      CreateSuggestionsWithUndoOrClearEntry(/*clear_form_offset=*/2);
  // Force creation of controller and view.
  client().popup_controller(manager());
  EXPECT_CALL(*client().popup_view(), Show);
  ShowSuggestions(manager(), suggestions);

  EXPECT_THAT(client().popup_controller(manager()).GetSuggestions(),
              ElementsAre(suggestions[2], suggestions[0], suggestions[1],
                          suggestions[3]));
}

TEST_F(AutofillKeyboardAccessoryControllerImplTest,
       UseAdditionalLabelForElidedLabel) {
  auto label_is = [](std::u16string label) {
    return ElementsAre(ElementsAre(Suggestion::Text(std::move(label))));
  };

  ShowSuggestions(manager(), CreateSuggestionsWithUndoOrClearEntry(
                                 /*clear_form_offset=*/1));

  // The 1st item is usually not visible (something like clear form) and has an
  // empty label. But it needs to be handled since UI might ask for it anyway.
  EXPECT_THAT(client().popup_controller(manager()).GetSuggestionLabelsAt(0),
              label_is(std::u16string()));

  // If there is a label, use it but cap at 8 bullets.
  EXPECT_THAT(client().popup_controller(manager()).GetSuggestionLabelsAt(1),
              label_is(u"********"));

  // If the label is empty, use the additional label:
  EXPECT_THAT(client().popup_controller(manager()).GetSuggestionLabelsAt(2),
              label_is(u"psl.origin.eg ********"));

  // If the password has less than 8 bullets, show the exact amount.
  EXPECT_THAT(client().popup_controller(manager()).GetSuggestionLabelsAt(3),
              label_is(u"***"));
}

}  // namespace autofill