blob: a386a3945676acc1cfadd6bb12e361d8a8698cce [file] [log] [blame]
// Copyright 2023 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/compose/chrome_compose_client.h"
#include <memory>
#include <utility>
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversion_utils.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/protobuf_matchers.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chrome/browser/compose/compose_enabling.h"
#include "chrome/browser/optimization_guide/mock_optimization_guide_keyed_service.h"
#include "chrome/browser/segmentation_platform/segmentation_platform_service_factory.h"
#include "chrome/browser/ui/hats/hats_service_factory.h"
#include "chrome/browser/ui/hats/mock_hats_service.h"
#include "chrome/browser/ui/hats/survey_config.h"
#include "chrome/common/compose/compose.mojom.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/content/browser/content_autofill_client.h"
#include "components/autofill/content/browser/content_autofill_driver.h"
#include "components/autofill/content/browser/test_autofill_client_injector.h"
#include "components/autofill/content/browser/test_autofill_manager_injector.h"
#include "components/autofill/content/browser/test_content_autofill_client.h"
#include "components/autofill/core/browser/filling/filling_product.h"
#include "components/autofill/core/browser/foundations/test_autofill_manager_waiter.h"
#include "components/autofill/core/browser/foundations/test_browser_autofill_manager.h"
#include "components/autofill/core/browser/suggestions/suggestion.h"
#include "components/autofill/core/browser/suggestions/suggestion_type.h"
#include "components/autofill/core/browser/test_utils/autofill_test_utils.h"
#include "components/autofill/core/common/aliases.h"
#include "components/autofill/core/common/autofill_test_utils.h"
#include "components/autofill/core/common/form_data.h"
#include "components/autofill/core/common/form_data_test_api.h"
#include "components/autofill/core/common/form_field_data.h"
#include "components/autofill/core/common/unique_ids.h"
#include "components/compose/core/browser/compose_features.h"
#include "components/compose/core/browser/compose_hats_utils.h"
#include "components/compose/core/browser/compose_metrics.h"
#include "components/compose/core/browser/config.h"
#include "components/optimization_guide/core/mock_optimization_guide_model_executor.h"
#include "components/optimization_guide/core/model_quality/model_quality_log_entry.h"
#include "components/optimization_guide/core/model_quality/test_model_quality_logs_uploader_service.h"
#include "components/optimization_guide/core/optimization_guide_features.h"
#include "components/optimization_guide/core/optimization_guide_model_executor.h"
#include "components/optimization_guide/core/optimization_guide_proto_util.h"
#include "components/optimization_guide/proto/features/compose.pb.h"
#include "components/optimization_guide/proto/model_execution.pb.h"
#include "components/optimization_guide/proto/model_quality_metadata.pb.h"
#include "components/optimization_guide/proto/model_quality_service.pb.h"
#include "components/prefs/pref_service.h"
#include "components/segmentation_platform/public/constants.h"
#include "components/segmentation_platform/public/testing/mock_segmentation_platform_service.h"
#include "components/ukm/test_ukm_recorder.h"
#include "components/unified_consent/pref_names.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents_user_data.h"
#include "content/public/test/test_renderer_host.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/network/test/test_network_connection_tracker.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using ::base::test::EqualsProto;
using ::base::test::RunOnceCallback;
using ::testing::_;
using ::testing::NiceMock;
using ComposeCallback = ::base::OnceCallback<void(const std::u16string&)>;
using ::optimization_guide::MockSession;
using ::optimization_guide::ModelQualityLogEntry;
using ::optimization_guide::OptimizationGuideModelExecutionError;
using ::optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback;
using ::optimization_guide::OptimizationGuideModelStreamingExecutionResult;
using ::optimization_guide::StreamingResponse;
using ::optimization_guide::TestModelQualityLogsUploaderService;
using ::optimization_guide::proto::LogAiDataRequest;
using ::optimization_guide::proto::ModelExecutionInfo;
using ::segmentation_platform::MockSegmentationPlatformService;
const uint64_t kSessionIdHigh = 1234;
const uint64_t kSessionIdLow = 5678;
const segmentation_platform::TrainingRequestId kTrainingRequestId =
segmentation_platform::TrainingRequestId(456);
class MockInnerText : public InnerTextProvider {
public:
MOCK_METHOD(void,
GetInnerText,
(content::RenderFrameHost & host,
std::optional<int> node_id,
content_extraction::InnerTextCallback callback));
};
class MockComposeDialog : public compose::mojom::ComposeUntrustedDialog {
public:
MOCK_METHOD(void,
ResponseReceived,
(compose::mojom::ComposeResponsePtr response));
MOCK_METHOD(void,
PartialResponseReceived,
(compose::mojom::PartialComposeResponsePtr response));
};
} // namespace
class ChromeComposeClientTest : public BrowserWithTestWindowTest {
public:
ChromeComposeClientTest()
: BrowserWithTestWindowTest(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
TestingProfile::TestingFactories GetTestingFactories() override {
return {
TestingProfile::TestingFactory{
segmentation_platform::SegmentationPlatformServiceFactory::
GetInstance(),
base::BindRepeating([](content::BrowserContext* context)
-> std::unique_ptr<KeyedService> {
return std::make_unique<
testing::NiceMock<MockSegmentationPlatformService>>();
})},
TestingProfile::TestingFactory{
OptimizationGuideKeyedServiceFactory::GetInstance(),
base::BindRepeating([](content::BrowserContext* context)
-> std::unique_ptr<KeyedService> {
return std::make_unique<
testing::NiceMock<MockOptimizationGuideKeyedService>>();
})},
};
}
void SetUp() override {
scoped_compose_enabled_ = ComposeEnabling::ScopedEnableComposeForTesting();
BrowserWithTestWindowTest::SetUp();
mock_hats_service_ = static_cast<MockHatsService*>(
HatsServiceFactory::GetInstance()->SetTestingFactoryAndUse(
GetProfile(), base::BindRepeating(&BuildMockHatsService)));
EXPECT_CALL(*mock_hats_service(), CanShowAnySurvey(_))
.WillRepeatedly(testing::Return(true));
scoped_feature_list_.InitWithFeatures(
{compose::features::kEnableCompose,
optimization_guide::features::kOptimizationGuideModelExecution},
{});
// Needed for feature params to reset.
compose::ResetConfigForTesting();
ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
GetOptimizationGuide().SetModelQualityLogsUploaderServiceForTesting(
std::make_unique<TestModelQualityLogsUploaderService>(
TestingBrowserProcess::GetGlobal()->local_state()));
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
true);
SetPrefsForComposeMSBBState(true);
AddTab(browser(), GetPageUrl());
client_ = ChromeComposeClient::FromWebContents(web_contents());
client_->SetModelExecutorForTest(&model_executor_);
client_->SetModelQualityLogsUploaderServiceForTest(
GetOptimizationGuide().GetModelQualityLogsUploaderService());
client_->SetInnerTextProviderForTest(&model_inner_text_);
client_->SetSkipShowDialogForTest(true);
client_->SetSessionIdForTest(base::Token(kSessionIdHigh, kSessionIdLow));
ON_CALL(model_inner_text(), GetInnerText(_, _, _))
.WillByDefault(testing::WithArg<2>(testing::Invoke(
[&](content_extraction::InnerTextCallback callback) {
std::unique_ptr<content_extraction::InnerTextResult>
expected_inner_text =
std::make_unique<content_extraction::InnerTextResult>("",
0);
std::move(callback).Run(std::move(expected_inner_text));
})));
ON_CALL(model_executor_, StartSession(_, _)).WillByDefault([&] {
return std::make_unique<NiceMock<MockSession>>(&session());
});
ON_CALL(session(), ExecuteModel(_, _))
.WillByDefault(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
std::move(callback),
OptimizationGuideModelStreamingExecutionResult(
base::ok(OptimizationGuideResponse(
ComposeResponse(true, "Cucumbers"))),
/*provided_by_on_device=*/false,
std::make_unique<optimization_guide::proto::
ModelExecutionInfo>())));
})));
ON_CALL(GetSegmentationPlatformService(),
GetClassificationResult(_, _, _, _))
.WillByDefault(testing::WithArg<3>(testing::Invoke(
[](segmentation_platform::ClassificationResultCallback callback) {
auto result = segmentation_platform::ClassificationResult(
segmentation_platform::PredictionStatus::kSucceeded);
result.request_id = kTrainingRequestId;
result.ordered_labels = {
segmentation_platform::kComposePrmotionLabelShow};
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), result));
})));
ON_CALL(GetOptimizationGuide(),
CanApplyOptimization(
_, optimization_guide::proto::OptimizationType::COMPOSE,
testing::An<optimization_guide::OptimizationMetadata*>()))
.WillByDefault(
[](const GURL& url,
optimization_guide::proto::OptimizationType optimization_type,
optimization_guide::OptimizationMetadata* metadata)
-> optimization_guide::OptimizationGuideDecision {
*metadata = {};
compose::ComposeHintMetadata compose_hint_metadata;
compose_hint_metadata.set_decision(
compose::ComposeHintDecision::COMPOSE_HINT_DECISION_ENABLED);
metadata->set_any_metadata(
optimization_guide::AnyWrapProto(compose_hint_metadata));
return optimization_guide::OptimizationGuideDecision::kTrue;
});
}
void TearDown() override {
// Clear default actions for safe teardown.
mock_hats_service_ = nullptr;
testing::Mock::VerifyAndClear(&GetSegmentationPlatformService());
client_ = nullptr;
scoped_feature_list_.Reset();
ukm_recorder_.reset();
// Needed for feature params to reset.
compose::ResetConfigForTesting();
BrowserWithTestWindowTest::TearDown();
}
void SetPrefsForComposeMSBBState(bool msbb_state) {
PrefService* prefs = GetProfile()->GetPrefs();
prefs->SetBoolean(
unified_consent::prefs::kUrlKeyedAnonymizedDataCollectionEnabled,
msbb_state);
}
void EnableAutoCompose() {
scoped_feature_list_.Reset();
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/{compose::features::kEnableCompose,
optimization_guide::features::
kOptimizationGuideModelExecution,
compose::features::kComposeAutoSubmit},
/*disabled_features=*/{});
// Needed for feature params to apply.
compose::ResetConfigForTesting();
}
void ShowDialogAndBindMojo(ComposeCallback callback = base::NullCallback()) {
ShowDialogAndBindMojoWithFieldData(field_data(), std::move(callback));
}
void ShowDialogAndBindMojoWithFieldData(
autofill::FormFieldData field_data,
ComposeCallback callback = base::NullCallback(),
autofill::AutofillComposeDelegate::UiEntryPoint entry_point =
autofill::AutofillComposeDelegate::UiEntryPoint::kContextMenu) {
client().ShowComposeDialog(entry_point, field_data, std::nullopt,
std::move(callback));
BindMojo();
}
void BindMojo() {
client_page_handler_.reset();
page_handler_.reset();
// Setup Dialog Page Handler.
mojo::PendingReceiver<compose::mojom::ComposeClientUntrustedPageHandler>
client_page_handler_pending_receiver =
client_page_handler_.BindNewPipeAndPassReceiver();
mojo::PendingReceiver<compose::mojom::ComposeSessionUntrustedPageHandler>
page_handler_pending_receiver =
page_handler_.BindNewPipeAndPassReceiver();
// Setup Compose Dialog.
callback_router_.reset();
callback_router_ = std::make_unique<
mojo::Receiver<compose::mojom::ComposeUntrustedDialog>>(
&compose_dialog());
mojo::PendingRemote<compose::mojom::ComposeUntrustedDialog>
callback_router_pending_remote =
callback_router_->BindNewPipeAndPassRemote();
// Bind mojo to client.
client_->BindComposeDialog(std::move(client_page_handler_pending_receiver),
std::move(page_handler_pending_receiver),
std::move(callback_router_pending_remote));
}
void FlushMojo() {
client_page_handler().FlushForTesting();
page_handler().FlushForTesting();
}
ChromeComposeClient& client() { return *client_; }
optimization_guide::MockSession& session() { return session_; }
MockInnerText& model_inner_text() { return model_inner_text_; }
MockComposeDialog& compose_dialog() { return compose_dialog_; }
autofill::FormFieldData& field_data() { return field_data_; }
// Get the WebContents for the first browser tab.
content::WebContents* web_contents() {
return browser()->tab_strip_model()->GetWebContentsAt(0);
}
mojo::Remote<compose::mojom::ComposeClientUntrustedPageHandler>&
client_page_handler() {
return client_page_handler_;
}
ukm::TestAutoSetUkmRecorder& ukm_recorder() { return *ukm_recorder_; }
mojo::Remote<compose::mojom::ComposeSessionUntrustedPageHandler>&
page_handler() {
return page_handler_;
}
GURL GetPageUrl() { return GURL("http://foo/1"); }
void SetSelection(const std::u16string& selection) {
field_data().set_selected_text(selection);
}
// Emulate selected text truncation performed by Autofill.
void SetSelectionWithTruncation(const std::u16string& selection,
size_t max_length) {
field_data().set_selected_text(selection.substr(0, max_length));
}
MockSegmentationPlatformService& GetSegmentationPlatformService() {
return *static_cast<MockSegmentationPlatformService*>(
segmentation_platform::SegmentationPlatformServiceFactory::
GetForProfile(GetProfile()));
}
MockOptimizationGuideKeyedService& GetOptimizationGuide() {
return *static_cast<MockOptimizationGuideKeyedService*>(
OptimizationGuideKeyedServiceFactory::GetForProfile(GetProfile()));
}
protected:
optimization_guide::proto::ComposePageMetadata ComposePageMetadata() {
optimization_guide::proto::ComposePageMetadata page_metadata;
page_metadata.set_page_url(GetPageUrl().spec());
page_metadata.set_page_title(base::UTF16ToUTF8(
browser()->tab_strip_model()->GetWebContentsAt(0)->GetTitle()));
return page_metadata;
}
optimization_guide::proto::ComposeRequest ComposeRequest(
std::string user_input,
optimization_guide::proto::ComposeUpfrontInputMode mode) {
optimization_guide::proto::ComposeRequest request;
request.mutable_generate_params()->set_user_input(user_input);
request.mutable_generate_params()->set_upfront_input_mode(mode);
return request;
}
optimization_guide::proto::ComposeRequest RegenerateRequest(
std::string previous_response) {
optimization_guide::proto::ComposeRequest request;
request.mutable_rewrite_params()->set_regenerate(true);
request.mutable_rewrite_params()->set_previous_response(previous_response);
return request;
}
optimization_guide::proto::ComposeResponse ComposeResponse(
bool ok,
std::string output) {
optimization_guide::proto::ComposeResponse response;
response.set_output(ok ? output : "");
return response;
}
StreamingResponse OptimizationGuideResponse(
const optimization_guide::proto::ComposeResponse compose_response,
bool is_complete = true) {
return StreamingResponse{
.response = optimization_guide::AnyWrapProto(compose_response),
.is_complete = is_complete,
};
}
OptimizationGuideModelStreamingExecutionResult
OptimizationGuideStreamingResult(
const optimization_guide::proto::ComposeResponse compose_response,
bool is_complete = true,
bool provided_by_on_device = false) {
return OptimizationGuideModelStreamingExecutionResult(
base::ok(OptimizationGuideResponse(compose_response, is_complete)),
provided_by_on_device);
}
const base::HistogramTester& histograms() const { return histogram_tester_; }
const base::UserActionTester& user_action_tester() const {
return user_action_tester_;
}
TestModelQualityLogsUploaderService& logs_uploader() {
return *static_cast<TestModelQualityLogsUploaderService*>(
GetOptimizationGuide().GetModelQualityLogsUploaderService());
}
const std::vector<std::unique_ptr<LogAiDataRequest>>& uploaded_logs() {
return logs_uploader().uploaded_logs();
}
// This helper function is a shortcut to adding a test future to listen for
// compose responses.
void BindComposeFutureToOnResponseReceived(
base::test::TestFuture<compose::mojom::ComposeResponsePtr>&
compose_future) {
ON_CALL(compose_dialog(), ResponseReceived(_))
.WillByDefault(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
compose_future.SetValue(std::move(response));
}));
}
autofill::TestBrowserAutofillManager* autofill_manager() {
return autofill_manager_injector_[web_contents()];
}
autofill::TestContentAutofillClient* autofill_client() {
return autofill_client_injector_[web_contents()];
}
base::test::ScopedFeatureList scoped_feature_list_;
MockHatsService* mock_hats_service() { return mock_hats_service_; }
private:
base::ScopedMockElapsedTimersForTest test_timer_;
raw_ptr<ChromeComposeClient> client_;
testing::NiceMock<optimization_guide::MockOptimizationGuideModelExecutor>
model_executor_;
testing::NiceMock<MockInnerText> model_inner_text_;
testing::NiceMock<optimization_guide::MockSession> session_;
testing::NiceMock<MockComposeDialog> compose_dialog_;
autofill::FormFieldData field_data_;
raw_ptr<content::WebContents> contents_;
base::HistogramTester histogram_tester_;
base::UserActionTester user_action_tester_;
autofill::test::AutofillUnitTestEnvironment autofill_test_environment_;
autofill::TestAutofillClientInjector<autofill::TestContentAutofillClient>
autofill_client_injector_;
autofill::TestAutofillManagerInjector<autofill::TestBrowserAutofillManager>
autofill_manager_injector_;
std::unique_ptr<mojo::Receiver<compose::mojom::ComposeUntrustedDialog>>
callback_router_;
std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
mojo::Remote<compose::mojom::ComposeClientUntrustedPageHandler>
client_page_handler_;
mojo::Remote<compose::mojom::ComposeSessionUntrustedPageHandler>
page_handler_;
ComposeEnabling::ScopedOverride scoped_compose_enabled_;
raw_ptr<MockHatsService> mock_hats_service_;
};
TEST_F(ChromeComposeClientTest, TestCompose) {
// Simulate page showing context menu.
auto* rfh =
browser()->tab_strip_model()->GetWebContentsAt(0)->GetPrimaryMainFrame();
content::ContextMenuParams params;
params.is_content_editable_for_autofill = true;
params.frame_origin = rfh->GetMainFrame()->GetLastCommittedOrigin();
EXPECT_TRUE(client().ShouldTriggerContextMenu(rfh, params));
// Then simulate clicking the dialog.
ShowDialogAndBindMojo();
// Now call Compose, checking the results.
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
EXPECT_EQ("Cucumbers", result->result);
EXPECT_FALSE(result->on_device_evaluation_used);
// Check that the session entry point histogram is recorded.
histograms().ExpectUniqueSample(compose::kComposeStartSessionEntryPoint,
compose::ComposeEntryPoint::kContextMenu, 1);
// Check that a user action for the Compose request was emitted.
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.ComposeRequest.CreateClicked"));
histograms().ExpectUniqueSample(
compose::kComposeRequestReason,
compose::ComposeRequestReason::kFirstRequestPolishMode, 1);
histograms().ExpectUniqueSample(
"Compose.Server.Request.Reason",
compose::ComposeRequestReason::kFirstRequestPolishMode, 1);
// Check that a request result OK metric was emitted.
histograms().ExpectUniqueSample(compose::kComposeRequestStatus,
compose::mojom::ComposeStatus::kOk, 1);
histograms().ExpectUniqueSample("Compose.Server.Request.Status",
compose::mojom::ComposeStatus::kOk, 1);
// Check that a request duration OK metric was emitted.
histograms().ExpectTotalCount(
base::StrCat({"Compose", compose::kComposeRequestDurationOkSuffix}), 1);
histograms().ExpectTotalCount(
base::StrCat(
{"Compose.Server", compose::kComposeRequestDurationOkSuffix}),
1);
// Check that no request duration Error metrics were emitted.
histograms().ExpectTotalCount(
base::StrCat({"Compose", compose::kComposeRequestDurationErrorSuffix}),
0);
histograms().ExpectTotalCount(
base::StrCat(
{"Compose.Server", compose::kComposeRequestDurationErrorSuffix}),
0);
// Check that the request metadata had a valid node offset.
histograms().ExpectUniqueSample(
compose::kInnerTextNodeOffsetFound,
compose::ComposeInnerTextNodeOffset::kOffsetFound, 1);
// Simulate insert call from Compose dialog.
page_handler()->AcceptComposeResult(base::NullCallback());
client_page_handler()->CloseUI(compose::mojom::CloseReason::kInsertButton);
FlushMojo();
// Check Compose Session Event Counts.
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
"Compose.Server.Session.EventCounts",
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
"Compose.OnDevice.Session.EventCounts",
compose::ComposeSessionEventTypes::kMainDialogShown, 0);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kComposeDialogOpened, 1);
histograms().ExpectBucketCount(
"Compose.Server.Session.EventCounts",
compose::ComposeSessionEventTypes::kComposeDialogOpened, 1);
histograms().ExpectBucketCount(
"Compose.OnDevice.Session.EventCounts",
compose::ComposeSessionEventTypes::kComposeDialogOpened, 0);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kCreateClicked, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kInsertClicked, 1);
histograms().ExpectUniqueSample("Compose.Session.EvalLocation",
compose::SessionEvalLocation::kServer, 1);
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check page level UKM metrics.
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kMenuItemShownName,
ukm::builders::Compose_PageEvents::kComposeTextInsertedName});
EXPECT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(
ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(ukm::builders::Compose_PageEvents::kMenuItemShownName,
1),
testing::Pair(
ukm::builders::Compose_PageEvents::kComposeTextInsertedName, 1)));
// Check session level UKM metrics.
auto session_ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_SessionProgress::kEntryName,
{ukm::builders::Compose_SessionProgress::kComposeCountName,
ukm::builders::Compose_SessionProgress::kDialogShownCountName,
ukm::builders::Compose_SessionProgress::kDialogShownCountName,
ukm::builders::Compose_SessionProgress::kUndoCountName,
ukm::builders::Compose_SessionProgress::kRegenerateCountName,
ukm::builders::Compose_SessionProgress::kShortenCountName,
ukm::builders::Compose_SessionProgress::kLengthenCountName,
ukm::builders::Compose_SessionProgress::kFormalCountName,
ukm::builders::Compose_SessionProgress::kCasualCountName,
ukm::builders::Compose_SessionProgress::kInsertedResultsName,
ukm::builders::Compose_SessionProgress::kCanceledName});
EXPECT_EQ(session_ukm_entries.size(), 1UL);
EXPECT_THAT(
session_ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(
ukm::builders::Compose_SessionProgress::kComposeCountName, 1),
testing::Pair(
ukm::builders::Compose_SessionProgress::kDialogShownCountName, 1),
testing::Pair(ukm::builders::Compose_SessionProgress::kUndoCountName,
0),
testing::Pair(
ukm::builders::Compose_SessionProgress::kRegenerateCountName, 0),
testing::Pair(
ukm::builders::Compose_SessionProgress::kShortenCountName, 0),
testing::Pair(
ukm::builders::Compose_SessionProgress::kLengthenCountName, 0),
testing::Pair(
ukm::builders::Compose_SessionProgress::kFormalCountName, 0),
testing::Pair(
ukm::builders::Compose_SessionProgress::kCasualCountName, 0),
testing::Pair(
ukm::builders::Compose_SessionProgress::kInsertedResultsName, 1),
testing::Pair(ukm::builders::Compose_SessionProgress::kCanceledName,
0)));
}
TEST_F(ChromeComposeClientTest, TestComposeServerAndOnDeviceResponses) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
EXPECT_EQ("Cucumbers", result->result);
EXPECT_FALSE(result->on_device_evaluation_used);
// Simulate rewrite, serviced by on-device model.
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Tomatoes"), true,
/*provided_by_on_device=*/true));
})));
page_handler()->Rewrite(compose::mojom::StyleModifier::kRetry);
// Simulate insert call from Compose dialog.
page_handler()->AcceptComposeResult(base::NullCallback());
client_page_handler()->CloseUI(compose::mojom::CloseReason::kInsertButton);
FlushMojo();
histograms().ExpectUniqueSample("Compose.Session.EvalLocation",
compose::SessionEvalLocation::kMixed, 1);
histograms().ExpectBucketCount(
compose::kComposeRequestReason,
compose::ComposeRequestReason::kFirstRequestPolishMode, 1);
histograms().ExpectUniqueSample(
"Compose.Server.Request.Reason",
compose::ComposeRequestReason::kFirstRequestPolishMode, 1);
histograms().ExpectBucketCount(compose::kComposeRequestReason,
compose::ComposeRequestReason::kRetryRequest,
1);
histograms().ExpectUniqueSample("Compose.OnDevice.Request.Reason",
compose::ComposeRequestReason::kRetryRequest,
1);
// Check that only the location agnostic metrics are recorded.
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
"Compose.Server.Session.EventCounts",
compose::ComposeSessionEventTypes::kMainDialogShown, 0);
histograms().ExpectBucketCount(
"Compose.OnDevice.Session.EventCounts",
compose::ComposeSessionEventTypes::kMainDialogShown, 0);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kComposeDialogOpened, 1);
histograms().ExpectBucketCount(
"Compose.Server.Session.EventCounts",
compose::ComposeSessionEventTypes::kComposeDialogOpened, 0);
histograms().ExpectBucketCount(
"Compose.OnDevice.Session.EventCounts",
compose::ComposeSessionEventTypes::kComposeDialogOpened, 0);
}
TEST_F(ChromeComposeClientTest, TestComposeOnDeviceSessionHistograms) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
// Simulate rewrite, serviced by on-device model.
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Tomatoes"), true,
/*provided_by_on_device=*/true));
})));
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
EXPECT_EQ("Tomatoes", result->result);
EXPECT_TRUE(result->on_device_evaluation_used);
// Simulate insert call from Compose dialog.
page_handler()->AcceptComposeResult(base::NullCallback());
client_page_handler()->CloseUI(compose::mojom::CloseReason::kInsertButton);
FlushMojo();
histograms().ExpectUniqueTimeSample(
"Compose.OnDevice.Session.Duration.Inserted",
base::ScopedMockElapsedTimersForTest::kMockElapsedTime, 1);
histograms().ExpectUniqueSample(
"Compose.OnDevice.Session.DialogShownCount.Accepted", 1, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
"Compose.Server.Session.EventCounts",
compose::ComposeSessionEventTypes::kMainDialogShown, 0);
histograms().ExpectBucketCount(
"Compose.OnDevice.Session.EventCounts",
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kComposeDialogOpened, 1);
histograms().ExpectBucketCount(
"Compose.Server.Session.EventCounts",
compose::ComposeSessionEventTypes::kComposeDialogOpened, 0);
histograms().ExpectBucketCount(
"Compose.OnDevice.Session.EventCounts",
compose::ComposeSessionEventTypes::kComposeDialogOpened, 1);
}
TEST_F(ChromeComposeClientTest, TestComposeEmptySession) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
client_page_handler()->CloseUI(compose::mojom::CloseReason::kInsertButton);
FlushMojo();
histograms().ExpectUniqueSample("Compose.Session.EvalLocation",
compose::SessionEvalLocation::kNone, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kComposeDialogOpened, 1);
}
TEST_F(ChromeComposeClientTest, TestComposeShowContextMenu) {
auto* rfh =
browser()->tab_strip_model()->GetWebContentsAt(0)->GetPrimaryMainFrame();
content::ContextMenuParams params;
params.is_content_editable_for_autofill = true;
params.frame_origin = rfh->GetMainFrame()->GetLastCommittedOrigin();
EXPECT_TRUE(client().ShouldTriggerContextMenu(rfh, params));
NavigateAndCommitActiveTab(GURL("about:blank"));
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kMenuItemShownName,
ukm::builders::Compose_PageEvents::kComposeTextInsertedName});
EXPECT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(
ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(ukm::builders::Compose_PageEvents::kMenuItemShownName,
1),
testing::Pair(
ukm::builders::Compose_PageEvents::kComposeTextInsertedName, 0)));
// Now show context menu twice on same page and verify that second UKM record
// reflects this.
EXPECT_TRUE(client().ShouldTriggerContextMenu(rfh, params));
EXPECT_TRUE(client().ShouldTriggerContextMenu(rfh, params));
NavigateAndCommitActiveTab(GURL("about:blank"));
ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kMenuItemShownName,
ukm::builders::Compose_PageEvents::kComposeTextInsertedName});
EXPECT_EQ(ukm_entries.size(), 2UL);
EXPECT_THAT(
ukm_entries[1].metrics,
testing::UnorderedElementsAre(
testing::Pair(ukm::builders::Compose_PageEvents::kMenuItemShownName,
2),
testing::Pair(
ukm::builders::Compose_PageEvents::kComposeTextInsertedName, 0)));
}
TEST_F(ChromeComposeClientTest, TestComposeShowContextMenuAndDialog) {
auto* rfh =
browser()->tab_strip_model()->GetWebContentsAt(0)->GetPrimaryMainFrame();
content::ContextMenuParams params;
params.is_content_editable_for_autofill = true;
params.frame_origin = rfh->GetMainFrame()->GetLastCommittedOrigin();
EXPECT_TRUE(client().ShouldTriggerContextMenu(rfh, params));
ShowDialogAndBindMojo();
NavigateAndCommitActiveTab(GURL("about:blank"));
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kMenuItemShownName,
ukm::builders::Compose_PageEvents::kComposeTextInsertedName,
ukm::builders::Compose_PageEvents::kProactiveNudgeShownName});
EXPECT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(
ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(ukm::builders::Compose_PageEvents::kMenuItemShownName,
1),
testing::Pair(
ukm::builders::Compose_PageEvents::kComposeTextInsertedName, 0),
testing::Pair(
ukm::builders::Compose_PageEvents::kProactiveNudgeShownName, 0)));
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kComposeDialogOpened, 1);
}
TEST_F(ChromeComposeClientTest, TestProactiveNudgeEngagementIsRecorded) {
// Enable and trigger the proactive nudge.
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = true;
config.proactive_nudge_show_probability = 1.0;
config.proactive_nudge_focus_delay = base::Microseconds(1);
config.proactive_nudge_segmentation = true;
config.proactive_nudge_always_collect_training_data = true;
autofill::FormData form_data;
form_data.set_url(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
autofill::FormFieldData& selected_field_data = test_api(form_data).field(0);
selected_field_data.set_origin(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged;
ASSERT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
task_environment()->FastForwardBy(config.proactive_nudge_focus_delay);
ASSERT_TRUE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
// Simulate clicking on the nudge to open compose.
ShowDialogAndBindMojoWithFieldData(
selected_field_data, base::NullCallback(),
autofill::AutofillComposeDelegate::UiEntryPoint::kAutofillPopup);
base::test::TestFuture<segmentation_platform::TrainingLabels> training_labels;
ukm::SourceId source =
web_contents()->GetPrimaryMainFrame()->GetPageUkmSourceId();
EXPECT_CALL(GetSegmentationPlatformService(),
CollectTrainingData(
segmentation_platform::proto::SegmentId::
OPTIMIZATION_TARGET_SEGMENTATION_COMPOSE_PROMOTION,
kTrainingRequestId, source, _, _))
.Times(1)
.WillOnce(testing::WithArg<3>(testing::Invoke(
[&](auto labels) { training_labels.SetValue(labels); })));
client().CloseUI(compose::mojom::CloseReason::kInsertButton);
// Trigger session deletion and verify that the engagement is recorded.
NavigateAndCommitActiveTab(GURL("about:blank"));
EXPECT_EQ(training_labels.Get().output_metric,
std::make_pair("Compose.ProactiveNudge.DerivedEngagement",
static_cast<base::HistogramBase::Sample32>(
compose::ProactiveNudgeDerivedEngagement::
kAcceptedComposeSuggestion)));
}
TEST_F(ChromeComposeClientTest,
TestShouldTriggerProactiveNudgeBlockedBySegmentation) {
// Enable and trigger the proactive nudge.
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = true;
config.proactive_nudge_show_probability = 1.0;
config.proactive_nudge_focus_delay = base::Microseconds(1);
config.proactive_nudge_segmentation = true;
autofill::FormData form_data;
form_data.set_url(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
autofill::FormFieldData& selected_field_data = test_api(form_data).field(0);
selected_field_data.set_origin(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_CALL(GetSegmentationPlatformService(),
GetClassificationResult(_, _, _, _))
.WillOnce(testing::WithArg<3>(testing::Invoke(
[](segmentation_platform::ClassificationResultCallback callback) {
auto result = segmentation_platform::ClassificationResult(
segmentation_platform::PredictionStatus::kSucceeded);
result.request_id = kTrainingRequestId;
result.ordered_labels = {
segmentation_platform::kComposePrmotionLabelDontShow};
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), result));
})));
// The initial trigger request comes from a text field change.
EXPECT_FALSE(client().ShouldTriggerPopup(
form_data, selected_field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(config.proactive_nudge_focus_delay);
// All remaining popup trigger requests come from the delayed nudge.
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kComposeDelayedProactiveNudge;
ASSERT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
histograms().ExpectBucketCount(
compose::kComposeProactiveNudgeShowStatus,
compose::ComposeShowStatus::kProactiveNudgeBlockedBySegmentationPlatform,
1);
// Commit metrics on page navigation.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check that the proactive nudge UKM was still captured.
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kMenuItemShownName,
ukm::builders::Compose_PageEvents::kComposeTextInsertedName,
ukm::builders::Compose_PageEvents::kProactiveNudgeShouldShowName,
ukm::builders::Compose_PageEvents::kProactiveNudgeShownName});
ASSERT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(
ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(ukm::builders::Compose_PageEvents::kMenuItemShownName,
0),
testing::Pair(
ukm::builders::Compose_PageEvents::kComposeTextInsertedName, 0),
testing::Pair(
ukm::builders::Compose_PageEvents::kProactiveNudgeShouldShowName,
1),
testing::Pair(
ukm::builders::Compose_PageEvents::kProactiveNudgeShownName, 0)));
// Check that even after a second call only one show status UMA was recorded.
EXPECT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
histograms().ExpectBucketCount(
compose::kComposeProactiveNudgeShowStatus,
compose::ComposeShowStatus::kProactiveNudgeBlockedBySegmentationPlatform,
1);
}
TEST_F(ChromeComposeClientTest, TestShouldTriggerProactiveNudgeDisabledUKM) {
// Disable the proactive nudge
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = false;
autofill::FormData form_data;
form_data.set_url(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
autofill::FormFieldData& selected_field_data = test_api(form_data).field(0);
selected_field_data.set_origin(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged;
// By default the proactive nudge is disabled.
EXPECT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
// Commit metrics on page navigation.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check that the proactive nudge UKM was still captured.
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kMenuItemShownName,
ukm::builders::Compose_PageEvents::kComposeTextInsertedName,
ukm::builders::Compose_PageEvents::kProactiveNudgeShouldShowName,
ukm::builders::Compose_PageEvents::kProactiveNudgeShownName});
ASSERT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(
ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(ukm::builders::Compose_PageEvents::kMenuItemShownName,
0),
testing::Pair(
ukm::builders::Compose_PageEvents::kComposeTextInsertedName, 0),
testing::Pair(
ukm::builders::Compose_PageEvents::kProactiveNudgeShouldShowName,
1),
testing::Pair(
ukm::builders::Compose_PageEvents::kProactiveNudgeShownName, 0)));
}
TEST_F(ChromeComposeClientTest, TestShouldTriggerProactiveNudgeEnabled) {
// Enable proactive nudge.
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = true;
config.proactive_nudge_focus_delay = base::Microseconds(4);
config.proactive_nudge_segmentation = false;
autofill::FormData form_data;
form_data.set_url(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
autofill::FormFieldData& selected_field_data = test_api(form_data).field(0);
selected_field_data.set_origin(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged;
EXPECT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
task_environment()->FastForwardBy(config.proactive_nudge_focus_delay);
EXPECT_TRUE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
// Commit metrics on page navigation.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check that the proactive nudge UKM was still captured.
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kMenuItemShownName,
ukm::builders::Compose_PageEvents::kComposeTextInsertedName,
ukm::builders::Compose_PageEvents::kProactiveNudgeShouldShowName,
ukm::builders::Compose_PageEvents::kProactiveNudgeShownName});
ASSERT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(
ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(ukm::builders::Compose_PageEvents::kMenuItemShownName,
0),
testing::Pair(
ukm::builders::Compose_PageEvents::kComposeTextInsertedName, 0),
testing::Pair(
ukm::builders::Compose_PageEvents::kProactiveNudgeShouldShowName,
1),
testing::Pair(
ukm::builders::Compose_PageEvents::kProactiveNudgeShownName, 1)));
// Check Compose.ProactiveNudge.CTR metrics.
histograms().ExpectBucketCount(compose::kComposeProactiveNudgeCtr,
compose::ComposeNudgeCtrEvent::kNudgeDisplayed,
1);
}
TEST_F(ChromeComposeClientTest,
TestShouldTriggerProactiveNudgePageChecksFailUKM) {
autofill::FormData form_data;
form_data.set_url(GURL("www.example.com"));
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
autofill::FormFieldData& selected_field_data = test_api(form_data).field(0);
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged;
// Will fail because field origin does not match page origin.
EXPECT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
// Commit metrics on page navigation.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check that the proactive nudge UKM was not captured.
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kProactiveNudgeShouldShowName});
ASSERT_EQ(ukm_entries.size(), 0UL);
}
TEST_F(ChromeComposeClientTest, TestProactiveNudgeMSBBDisabled) {
SetPrefsForComposeMSBBState(false);
autofill::FormData form_data;
form_data.set_url(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
autofill::FormFieldData& selected_field_data = test_api(form_data).field(0);
selected_field_data.set_origin(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged;
// Will fail because MSBB is not set
EXPECT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
histograms().ExpectBucketCount(
compose::kComposeProactiveNudgeShowStatus,
compose::ComposeShowStatus::kProactiveNudgeDisabledByMSBB, 1);
}
TEST_F(ChromeComposeClientTest, TestComposeShouldTriggerSavedStateNudgeUKM) {
autofill::FormData form_data;
form_data.set_url(GetPageUrl());
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
const autofill::FormFieldData& selected_field_data =
test_api(form_data).field(0);
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged;
// Start a Compose session on selected field.
ShowDialogAndBindMojoWithFieldData(selected_field_data);
// By default the saved state nudge is shown.
EXPECT_TRUE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
// Commit metrics on page navigation.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check that no proactive nudge UKM was recorded.
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kMenuItemShownName,
ukm::builders::Compose_PageEvents::kComposeTextInsertedName,
ukm::builders::Compose_PageEvents::kProactiveNudgeShouldShowName});
EXPECT_EQ(ukm_entries.size(), 0UL);
}
TEST_F(ChromeComposeClientTest, TestComposeWithIncompleteResponsesAnimated) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{optimization_guide::features::kOptimizationGuideOnDeviceModel,
compose::features::kComposeTextOutputAnimation},
{});
const std::string input = "a user typed this";
optimization_guide::proto::ComposeRequest context_request;
*context_request.mutable_page_metadata() = ComposePageMetadata();
optimization_guide::OptimizationGuideModelExecutionResultStreamingCallback
saved_callback;
EXPECT_CALL(session(), AddContext(EqualsProto(context_request)));
EXPECT_CALL(session(),
ExecuteModel(
EqualsProto(ComposeRequest(
input, optimization_guide::proto::
ComposeUpfrontInputMode::COMPOSE_POLISH_MODE)),
_))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
// Start with a partial response.
callback.Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucu"), /*is_complete=*/false,
/*provided_by_on_device=*/true));
saved_callback = callback;
})));
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::PartialComposeResponsePtr>
partial_future;
EXPECT_CALL(compose_dialog(), PartialResponseReceived(_))
.WillRepeatedly(testing::Invoke(
[&](compose::mojom::PartialComposeResponsePtr response) {
partial_future.SetValue(std::move(response));
}));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillRepeatedly(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
page_handler()->Compose(input, compose::mojom::InputMode::kPolish, false);
compose::mojom::PartialComposeResponsePtr partial_result =
partial_future.Take();
EXPECT_EQ("Cucu", partial_result->result);
// Request the initial state, and verify there's still a pending request.
base::test::TestFuture<compose::mojom::OpenMetadataPtr> initial_state_future;
page_handler()->RequestInitialState(initial_state_future.GetCallback());
compose::mojom::OpenMetadataPtr initial_state = initial_state_future.Take();
EXPECT_TRUE(initial_state->compose_state->has_pending_request);
// Then send the full response.
saved_callback.Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucumbers"), /*is_complete=*/true,
/*provided_by_on_device=*/true));
auto complete_result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, complete_result->status);
EXPECT_EQ("Cucumbers", complete_result->result);
EXPECT_TRUE(complete_result->on_device_evaluation_used);
// Check that a single request result OK metric was emitted.
histograms().ExpectUniqueSample(compose::kComposeRequestStatus,
compose::mojom::ComposeStatus::kOk, 1);
histograms().ExpectUniqueSample("Compose.OnDevice.Request.Status",
compose::mojom::ComposeStatus::kOk, 1);
// Check that a single request duration OK metric was emitted.
histograms().ExpectTotalCount(
base::StrCat({"Compose", compose::kComposeRequestDurationOkSuffix}), 1);
histograms().ExpectTotalCount(
base::StrCat(
{"Compose.OnDevice", compose::kComposeRequestDurationOkSuffix}),
1);
// Check that no request duration Error metric was emitted.
histograms().ExpectTotalCount(
base::StrCat({"Compose", compose::kComposeRequestDurationErrorSuffix}),
0);
}
TEST_F(ChromeComposeClientTest, TestComposeNoResultAnimation) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{optimization_guide::features::kOptimizationGuideOnDeviceModel}, {});
const std::string input = "a user typed this";
optimization_guide::proto::ComposeRequest context_request;
*context_request.mutable_page_metadata() = ComposePageMetadata();
base::test::TestFuture<
optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback>
saved_callback;
EXPECT_CALL(session(), AddContext(EqualsProto(context_request)));
EXPECT_CALL(session(),
ExecuteModel(
EqualsProto(ComposeRequest(
input, optimization_guide::proto::
ComposeUpfrontInputMode::COMPOSE_POLISH_MODE)),
_))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) { saved_callback.SetValue(callback); })));
ShowDialogAndBindMojo();
EXPECT_CALL(compose_dialog(), PartialResponseReceived(_)).Times(0);
EXPECT_CALL(compose_dialog(), ResponseReceived(_)).Times(1);
page_handler()->Compose(input, compose::mojom::InputMode::kPolish, false);
// Send a partial response.
saved_callback.Get().Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucu"), /*is_complete=*/false,
/*provided_by_on_device=*/true));
// Then send the full response.
saved_callback.Get().Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucumbers"), /*is_complete=*/true,
/*provided_by_on_device=*/true));
FlushMojo();
}
TEST_F(ChromeComposeClientTest, TestComposeSessionIgnoresPreviousResponse) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{optimization_guide::features::kOptimizationGuideOnDeviceModel,
compose::features::kComposeTextOutputAnimation},
{});
const std::string input = "a user typed this";
const std::string input2 = "another input";
optimization_guide::proto::ComposeRequest context_request;
*context_request.mutable_page_metadata() = ComposePageMetadata();
optimization_guide::OptimizationGuideModelExecutionResultStreamingCallback
original_callback;
EXPECT_CALL(session(), AddContext(EqualsProto(context_request)));
EXPECT_CALL(session(),
ExecuteModel(
EqualsProto(ComposeRequest(
input, optimization_guide::proto::
ComposeUpfrontInputMode::COMPOSE_POLISH_MODE)),
_))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
// Save the callback to call later.
original_callback = callback;
// Start with a partial response.
callback.Run(
OptimizationGuideStreamingResult(ComposeResponse(true, "Cucu"),
/*is_complete=*/false));
})));
EXPECT_CALL(
session(),
ExecuteModel(
EqualsProto(ComposeRequest(
input2, optimization_guide::proto::ComposeUpfrontInputMode::
COMPOSE_POLISH_MODE)),
_))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
// First call the original callback. This should be ignored.
original_callback.Run(
OptimizationGuideStreamingResult(ComposeResponse(true, "old")));
// Start with a partial response.
callback.Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucumbers")));
})));
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::PartialComposeResponsePtr>
partial_response;
EXPECT_CALL(compose_dialog(), PartialResponseReceived(_))
.WillRepeatedly(testing::Invoke(
[&](compose::mojom::PartialComposeResponsePtr response) {
partial_response.SetValue(std::move(response));
}));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> complete_response;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillRepeatedly(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
complete_response.SetValue(std::move(response));
}));
page_handler()->Compose(input, compose::mojom::InputMode::kPolish, false);
EXPECT_EQ("Cucu", partial_response.Get()->result);
page_handler()->Compose(input2, compose::mojom::InputMode::kPolish, false);
EXPECT_EQ(compose::mojom::ComposeStatus::kOk,
complete_response.Get()->status);
EXPECT_EQ("Cucumbers", complete_response.Get()->result);
// Check that a single request result OK metric was emitted.
histograms().ExpectUniqueSample(compose::kComposeRequestStatus,
compose::mojom::ComposeStatus::kOk, 1);
// Check that a single request duration OK metric was emitted.
histograms().ExpectTotalCount(
base::StrCat({"Compose", compose::kComposeRequestDurationOkSuffix}), 1);
// Check that no request duration Error metric was emitted.
histograms().ExpectTotalCount(
base::StrCat({"Compose", compose::kComposeRequestDurationErrorSuffix}),
0);
}
TEST_F(ChromeComposeClientTest, TestComposeRequestTimeout) {
// Set config such that requests time out immediately.
compose::Config& config = compose::GetMutableConfigForTesting();
config.request_latency_timeout = base::Seconds(0);
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kRequestTimeout, result->status);
histograms().ExpectUniqueSample(
compose::kComposeRequestStatus,
compose::mojom::ComposeStatus::kRequestTimeout, 1);
histograms().ExpectUniqueSample(
"Compose.Server.Request.Status",
compose::mojom::ComposeStatus::kRequestTimeout, 1);
}
TEST_F(ChromeComposeClientTest, TestComposeParams) {
ShowDialogAndBindMojo();
std::string user_input = "a user typed this";
auto matcher = EqualsProto(ComposeRequest(
user_input,
optimization_guide::proto::ComposeUpfrontInputMode::COMPOSE_POLISH_MODE));
EXPECT_CALL(session(), ExecuteModel(matcher, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucumbers")));
})));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillOnce(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
page_handler()->Compose(user_input, compose::mojom::InputMode::kPolish,
false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
NavigateAndCommitActiveTab(GURL("about:blank"));
}
TEST_F(ChromeComposeClientTest, TestComposeGenericServerError) {
ShowDialogAndBindMojo();
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(
OptimizationGuideModelStreamingExecutionResult(
base::unexpected(
OptimizationGuideModelExecutionError::
FromModelExecutionError(
OptimizationGuideModelExecutionError::
ModelExecutionError::kGenericFailure)),
false, std::make_unique<ModelExecutionInfo>()));
})));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillOnce(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kServerError, result->status);
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
EXPECT_TRUE(log_uploaded_signal.Wait());
// Check that the quality modeling log is still correct
ASSERT_EQ(1u, uploaded_logs().size());
const auto& session_id = uploaded_logs()[0]->compose().quality().session_id();
EXPECT_EQ(kSessionIdHigh, session_id.high());
EXPECT_EQ(kSessionIdLow, session_id.low());
// Check the expected event count metrics.
std::vector<std::pair<compose::ComposeSessionEventTypes, int>> event_counts =
{
{compose::ComposeSessionEventTypes::kComposeDialogOpened, 1},
{compose::ComposeSessionEventTypes::kMainDialogShown, 1},
{compose::ComposeSessionEventTypes::kFREShown, 0},
{compose::ComposeSessionEventTypes::kMSBBShown, 0},
{compose::ComposeSessionEventTypes::kCreateClicked, 1},
{compose::ComposeSessionEventTypes::kFailedRequest, 1},
};
for (auto [event_type, count] : event_counts) {
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
event_type, count);
histograms().ExpectBucketCount("Compose.Server.Session.EventCounts",
event_type, count);
histograms().ExpectBucketCount("Compose.OnDevice.Session.EventCounts",
event_type, 0);
}
}
TEST_F(ChromeComposeClientTest, TestComposeSetTriggeredFromModifierOnError) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
// Simulate rewrite producing an error response.
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(
OptimizationGuideModelStreamingExecutionResult(
base::unexpected(
OptimizationGuideModelExecutionError::
FromModelExecutionError(
OptimizationGuideModelExecutionError::
ModelExecutionError::kGenericFailure)),
false, std::make_unique<ModelExecutionInfo>()));
})));
page_handler()->Rewrite(compose::mojom::StyleModifier::kRetry);
result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kServerError, result->status);
EXPECT_TRUE(result->triggered_from_modifier);
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
}
// Tests that we return an error if Optimization Guide is unable to parse the
// response. In this case the response will be std::nullopt.
TEST_F(ChromeComposeClientTest, TestComposeNoParsedAny) {
ShowDialogAndBindMojo();
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](OptimizationGuideModelExecutionResultStreamingCallback callback) {
std::move(callback).Run(
OptimizationGuideModelStreamingExecutionResult(
base::ok(StreamingResponse{.is_complete = true}),
/*provided_by_on_device=*/false));
})));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillOnce(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kNoResponse, result->status);
// Check that a request result No Response metric was emitted.
histograms().ExpectUniqueSample(compose::kComposeRequestStatus,
compose::mojom::ComposeStatus::kNoResponse,
1);
// Check that a request duration Error metric was emitted.
histograms().ExpectTotalCount(
base::StrCat({"Compose", compose::kComposeRequestDurationErrorSuffix}),
1);
// Check that no request duration OK metric was emitted.
histograms().ExpectTotalCount(
base::StrCat({"Compose", compose::kComposeRequestDurationOkSuffix}), 0);
}
TEST_F(ChromeComposeClientTest, TestOptimizationGuideDisabled) {
scoped_feature_list_.Reset();
// Enable Compose and disable optimization guide model execution.
scoped_feature_list_.InitWithFeatures(
{compose::features::kEnableCompose},
{optimization_guide::features::kOptimizationGuideModelExecution});
ShowDialogAndBindMojo();
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(0);
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillOnce(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kMisconfiguration, result->status);
}
TEST_F(ChromeComposeClientTest, TestNoModelExecutor) {
client().SetModelExecutorForTest(nullptr);
ShowDialogAndBindMojo();
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(0);
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillOnce(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kMisconfiguration, result->status);
}
TEST_F(ChromeComposeClientTest, TestRestoreStateAfterRequestResponse) {
ShowDialogAndBindMojo();
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](OptimizationGuideModelExecutionResultStreamingCallback callback) {
std::move(callback).Run(
OptimizationGuideModelStreamingExecutionResult(
base::ok(OptimizationGuideResponse(
ComposeResponse(true, "Cucumbers"))),
false));
})));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillOnce(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_EQ("", result->compose_state->webui_state);
EXPECT_FALSE(result->compose_state->response.is_null());
EXPECT_EQ(compose::mojom::ComposeStatus::kOk,
result->compose_state->response->status);
EXPECT_EQ("Cucumbers", result->compose_state->response->result);
EXPECT_FALSE(result->compose_state->has_pending_request);
}
TEST_F(ChromeComposeClientTest, TestRestoreEmptyState) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_EQ("", result->compose_state->webui_state);
EXPECT_TRUE(result->compose_state->response.is_null());
EXPECT_FALSE(result->compose_state->has_pending_request);
}
// Tests that a saved WebUI state is properly returned.
TEST_F(ChromeComposeClientTest, TestSaveAndRestoreWebUIState) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::OpenMetadataPtr> test_future;
page_handler()->SaveWebUIState("web ui state");
page_handler()->RequestInitialState(test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = test_future.Take();
EXPECT_EQ("web ui state", result->compose_state->webui_state);
}
// Tests that the same saved WebUI state is returned after compose().
TEST_F(ChromeComposeClientTest, TestSaveThenComposeThenRestoreWebUIState) {
ShowDialogAndBindMojo();
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucumbers")));
})));
base::test::TestFuture<compose::mojom::ComposeResponsePtr>
compose_test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillOnce(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
compose_test_future.SetValue(std::move(response));
}));
page_handler()->SaveWebUIState("web ui state");
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr response = compose_test_future.Take();
EXPECT_FALSE(response->undo_available)
<< "First Compose() response should say undo not available.";
base::test::TestFuture<compose::mojom::OpenMetadataPtr> test_future;
page_handler()->RequestInitialState(test_future.GetCallback());
compose::mojom::OpenMetadataPtr open_metadata = test_future.Take();
EXPECT_EQ("web ui state", open_metadata->compose_state->webui_state);
}
TEST_F(ChromeComposeClientTest, NoStateWorksAtChromeCompose) {
NavigateAndCommitActiveTab(GURL(chrome::kChromeUIUntrustedComposeUrl));
// We skip showing the dialog here as there is no dialog required at this URL.
BindMojo();
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucumbers")));
})));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillOnce(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
EXPECT_EQ("Cucumbers", result->result);
}
// Tests that closing after showing the dialog does not crash the browser.
TEST_F(ChromeComposeClientTest, TestCloseUI) {
ShowDialogAndBindMojo();
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
}
// Tests that session level UKM metrics are properly captured after closing the
// dialog.
TEST_F(ChromeComposeClientTest, TestCancelUkmMetrics) {
ShowDialogAndBindMojo();
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
// Make sure the async call to CloseUI completes before navigating away.
FlushMojo();
// Navigate page away to upload UKM metrics to the collector.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check session level UKM metrics.
auto session_ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_SessionProgress::kEntryName,
{ukm::builders::Compose_SessionProgress::kCanceledName});
EXPECT_EQ(session_ukm_entries.size(), 1UL);
EXPECT_THAT(session_ukm_entries[0].metrics,
testing::UnorderedElementsAre(testing::Pair(
ukm::builders::Compose_SessionProgress::kCanceledName, 1)));
}
// Tests that closing the session at chrome-untrusted://compose does not crash
// the browser, even though there is no dialog shown at that URL.
TEST_F(ChromeComposeClientTest, TestCloseUIAtChromeCompose) {
NavigateAndCommitActiveTab(GURL(chrome::kChromeUIUntrustedComposeUrl));
// We skip showing the dialog here as there is no dialog required at this
// URL.
BindMojo();
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
}
// Tests that an unpaired high surrogate resulting from truncation by substr is
// properly removed.
TEST_F(ChromeComposeClientTest, TestOpenDialogWithTruncatedSelectedText) {
std::u16string input(u".🦄🦄🦄");
field_data().set_value(input);
SetSelectionWithTruncation(input, 6);
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_EQ(".🦄🦄", result->initial_input);
}
// Tests that opening the dialog with user selected text will return that text
// when the WebUI requests initial state.
TEST_F(ChromeComposeClientTest, TestOpenDialogWithSelectedText) {
field_data().set_value(u"user selected text");
SetSelection(u"selected text");
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_EQ("selected text", result->initial_input);
// Close session to record UMA
client().CloseUI(compose::mojom::CloseReason::kInsertButton);
// Check Compose Session Event Counts.
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kStartedWithSelection, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kInsertClicked, 1);
}
// Tests that opening the dialog with selected text from the proactive nudge
// will send that text to the WebUI dialog.
TEST_F(ChromeComposeClientTest,
TestOpenDialogWithSelectedTextFromProactiveNudge) {
field_data().set_value(u"user selected text");
SetSelection(u"selected text");
ShowDialogAndBindMojoWithFieldData(
field_data(), base::NullCallback(),
autofill::AutofillComposeDelegate::UiEntryPoint::kAutofillPopup);
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_EQ("selected text", result->initial_input);
// Close session to record UMA
client().CloseUI(compose::mojom::CloseReason::kInsertButton);
// Check that the session entry point histogram is recorded.
histograms().ExpectUniqueSample(compose::kComposeStartSessionEntryPoint,
compose::ComposeEntryPoint::kProactiveNudge,
1);
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.StartedSession.ProactiveNudge"));
// Check Compose Session Event Counts.
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kStartedWithSelection, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kInsertClicked, 1);
// Check Compose.ProactiveNudge.CTR metrics.
histograms().ExpectBucketCount(compose::kComposeProactiveNudgeCtr,
compose::ComposeNudgeCtrEvent::kDialogOpened,
1);
}
// Test that opening the saved state dialog with selected text does not start
// a new session or update the initial selection.
TEST_F(ChromeComposeClientTest, TestSelectedTextWithSavedStateNudge) {
field_data().set_value(u"this text is first and this text is second");
SetSelection(u"text is first");
ShowDialogAndBindMojo();
page_handler()->SaveWebUIState("web ui state");
// Flush mojo before next dialog open call so that web ui state is preserved.
FlushMojo();
// Change selection and re-open dialog from saved state popup. The new
// selection should be ignored.
SetSelection(u"text is second");
ShowDialogAndBindMojoWithFieldData(
field_data(), base::NullCallback(),
autofill::AutofillComposeDelegate::UiEntryPoint::kAutofillPopup);
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_EQ("web ui state", result->compose_state->webui_state);
EXPECT_EQ("text is first", result->initial_input);
EXPECT_TRUE(result->text_selected);
// Check that the session entry point histogram is recorded.
histograms().ExpectUniqueSample(compose::kComposeStartSessionEntryPoint,
compose::ComposeEntryPoint::kContextMenu, 1);
histograms().ExpectUniqueSample(compose::kComposeResumeSessionEntryPoint,
compose::ComposeEntryPoint::kSavedStateNudge,
1);
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.StartedSession.ContextMenu"));
}
TEST_F(ChromeComposeClientTest,
TestMultipleDialogOpensWithChangingSelectedText) {
field_data().set_value(u"this text is first and this text is second");
SetSelection(u"text is first");
ShowDialogAndBindMojo();
page_handler()->SaveWebUIState("web ui state");
// Flush mojo before next dialog open call so that web ui state is preserved.
FlushMojo();
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_EQ("web ui state", result->compose_state->webui_state);
EXPECT_EQ("text is first", result->initial_input);
EXPECT_TRUE(result->text_selected);
// Clear selection and re-open dialog from saved state popup.
SetSelection(u"");
ShowDialogAndBindMojoWithFieldData(
field_data(), base::NullCallback(),
autofill::AutofillComposeDelegate::UiEntryPoint::kAutofillPopup);
page_handler()->RequestInitialState(open_test_future.GetCallback());
result = open_test_future.Take();
EXPECT_EQ("web ui state", result->compose_state->webui_state);
EXPECT_EQ("text is first", result->initial_input);
// Web UI should now show that no text was selected when the dialog opened.
EXPECT_FALSE(result->text_selected);
}
// Tests that opening the dialog with selected text clears existing state.
TEST_F(ChromeComposeClientTest, TestClearStateWhenOpenWithSelectedText) {
ShowDialogAndBindMojo();
page_handler()->SaveWebUIState("web ui state");
// Flush mojo before next dialog open call so that web ui state is preserved.
FlushMojo();
field_data().set_value(u"user selected text");
SetSelection(u"selected text");
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_EQ("", result->compose_state->webui_state);
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.EndedSession.NewSessionWithSelectedText"));
histograms().ExpectUniqueSample(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kReplacedWithNewSession, 1);
}
TEST_F(ChromeComposeClientTest, InputModeUnsetHistogramTest) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
page_handler()->Compose("", compose::mojom::InputMode::kUnset, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
histograms().ExpectUniqueSample(compose::kComposeRequestReason,
compose::ComposeRequestReason::kFirstRequest,
1);
histograms().ExpectUniqueSample("Compose.Server.Request.Reason",
compose::ComposeRequestReason::kFirstRequest,
1);
}
TEST_F(ChromeComposeClientTest, InputModePolishHistogramTest) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
histograms().ExpectUniqueSample(
compose::kComposeRequestReason,
compose::ComposeRequestReason::kFirstRequestPolishMode, 1);
histograms().ExpectUniqueSample(
"Compose.Server.Request.Reason",
compose::ComposeRequestReason::kFirstRequestPolishMode, 1);
}
TEST_F(ChromeComposeClientTest, InputModeElaborateHistogramTest) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
page_handler()->Compose("", compose::mojom::InputMode::kElaborate, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
histograms().ExpectUniqueSample(
compose::kComposeRequestReason,
compose::ComposeRequestReason::kFirstRequestElaborateMode, 1);
histograms().ExpectUniqueSample(
"Compose.Server.Request.Reason",
compose::ComposeRequestReason::kFirstRequestElaborateMode, 1);
}
TEST_F(ChromeComposeClientTest, InputModeFormalizeHistogramTest) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
page_handler()->Compose("", compose::mojom::InputMode::kFormalize, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
histograms().ExpectUniqueSample(
compose::kComposeRequestReason,
compose::ComposeRequestReason::kFirstRequestFormalizeMode, 1);
histograms().ExpectUniqueSample(
"Compose.Server.Request.Reason",
compose::ComposeRequestReason::kFirstRequestFormalizeMode, 1);
}
TEST_F(ChromeComposeClientTest,
TestContextMenuNotRecordedAsProactiveInQualityLogs) {
field_data().set_value(u"user selected text");
ShowDialogAndBindMojoWithFieldData(
field_data(), base::NullCallback(),
autofill::AutofillComposeDelegate::UiEntryPoint::kContextMenu);
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
client().CloseUI(compose::mojom::CloseReason::kInsertButton);
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(1u, uploaded_logs().size());
EXPECT_FALSE(
uploaded_logs()[0]->compose().quality().started_with_proactive_nudge());
// Force reporting of page events UKM.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check that page events UKM is recorded for opening from the nudge.
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kComposeTextInsertedName,
ukm::builders::Compose_PageEvents::kProactiveNudgeOpenedName});
EXPECT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(
ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(
ukm::builders::Compose_PageEvents::kComposeTextInsertedName, 1),
testing::Pair(
ukm::builders::Compose_PageEvents::kProactiveNudgeOpenedName,
0)));
}
TEST_F(ChromeComposeClientTest, TestProactiveNudgeRecordedInQualityLogs) {
field_data().set_value(u"user selected text");
ShowDialogAndBindMojoWithFieldData(
field_data(), base::NullCallback(),
autofill::AutofillComposeDelegate::UiEntryPoint::kAutofillPopup);
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
BindComposeFutureToOnResponseReceived(test_future);
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
client().CloseUI(compose::mojom::CloseReason::kInsertButton);
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(1u, uploaded_logs().size());
EXPECT_TRUE(
uploaded_logs()[0]->compose().quality().started_with_proactive_nudge());
// Force reporting of page events UKM.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check that page events UKM does not record opening the proactive nudge.
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kComposeTextInsertedName,
ukm::builders::Compose_PageEvents::kProactiveNudgeOpenedName});
EXPECT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(
ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(
ukm::builders::Compose_PageEvents::kComposeTextInsertedName, 1),
testing::Pair(
ukm::builders::Compose_PageEvents::kProactiveNudgeOpenedName,
1)));
}
// Checks proper propagation of Compose config params.
TEST_F(ChromeComposeClientTest, TestInputParams) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.input_min_words = 5;
config.input_max_words = 20;
config.input_max_chars = 100;
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_EQ(5, result->configurable_params->min_word_limit);
EXPECT_EQ(20, result->configurable_params->max_word_limit);
EXPECT_EQ(100, result->configurable_params->max_character_limit);
}
// Tests that Undo is not possible when Compose is never called and no response
// is ever received.
TEST_F(ChromeComposeClientTest, TestEmptyUndo) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeStatePtr> test_future;
page_handler()->Undo(test_future.GetCallback());
EXPECT_FALSE(test_future.Take());
}
// Tests that Undo is not possible after only one Compose() invocation.
// TODO(b/334007229): incorporate redo testing.
TEST_F(ChromeComposeClientTest, TestUndoUnavailableFirstCompose) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr response = compose_future.Take();
EXPECT_FALSE(response->undo_available)
<< "First Compose() response should say undo not available.";
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_future;
page_handler()->RequestInitialState(open_future.GetCallback());
compose::mojom::OpenMetadataPtr open_metadata = open_future.Take();
EXPECT_FALSE(open_metadata->compose_state->response->undo_available)
<< "RequestInitialState() should return a response that undo is "
"not available after only one Compose() invocation.";
base::test::TestFuture<compose::mojom::ComposeStatePtr> undo_future;
page_handler()->Undo(undo_future.GetCallback());
compose::mojom::ComposeStatePtr state = undo_future.Take();
EXPECT_FALSE(state)
<< "Undo should return null after only one Compose() invocation.";
}
// Tests Undo after calling Compose() twice.
TEST_F(ChromeComposeClientTest, TestComposeTwiceThenUpdateWebUIStateThenUndo) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
page_handler()->SaveWebUIState("this state should be restored with undo");
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr response = compose_future.Take();
EXPECT_FALSE(response->undo_available) << "First Compose() response should "
"say undo is not available.";
page_handler()->SaveWebUIState("second state");
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
response = compose_future.Take();
EXPECT_TRUE(response->undo_available) << "Second Compose() response should "
"say undo is available.";
page_handler()->SaveWebUIState("user edited the input field further");
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_future;
page_handler()->RequestInitialState(open_future.GetCallback());
compose::mojom::OpenMetadataPtr open_metadata = open_future.Take();
EXPECT_TRUE(open_metadata->compose_state->response->undo_available)
<< "RequestInitialState() should return a response that undo is "
"available after second Compose() invocation.";
EXPECT_EQ("user edited the input field further",
open_metadata->compose_state->webui_state);
base::test::TestFuture<compose::mojom::ComposeStatePtr> undo_future;
page_handler()->Undo(undo_future.GetCallback());
compose::mojom::ComposeStatePtr state = undo_future.Take();
EXPECT_TRUE(state)
<< "Undo should return valid state after second Compose() invocation.";
EXPECT_EQ("this state should be restored with undo", state->webui_state);
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
// Make sure the async call to CloseUI() completes before navigating away.
FlushMojo();
// Check Compose Session Event Counts.
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kUndoClicked, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kCloseClicked, 1);
// Navigate page away to upload UKM metrics to the collector.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check session level UKM metrics.
auto session_ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_SessionProgress::kEntryName,
{ukm::builders::Compose_SessionProgress::kUndoCountName});
EXPECT_EQ(session_ukm_entries.size(), 1UL);
EXPECT_THAT(session_ukm_entries[0].metrics,
testing::UnorderedElementsAre(testing::Pair(
ukm::builders::Compose_SessionProgress::kUndoCountName, 1)));
}
// Tests if Undo can be done more than once.
// TODO(b/334007229): incorporate redo testing.
TEST_F(ChromeComposeClientTest, TestUndoStackMultipleUndos) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
page_handler()->SaveWebUIState("first state");
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr response = compose_future.Take();
EXPECT_FALSE(response->undo_available) << "First Compose() response should "
"say undo is not available.";
page_handler()->SaveWebUIState("second state");
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
response = compose_future.Take();
EXPECT_TRUE(response->undo_available) << "Second Compose() response should "
"say undo is available.";
page_handler()->SaveWebUIState("third state");
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
response = compose_future.Take();
EXPECT_TRUE(response->undo_available) << "Third Compose() response should "
"say undo is available.";
page_handler()->SaveWebUIState("fourth state");
base::test::TestFuture<compose::mojom::ComposeStatePtr> undo_future;
page_handler()->Undo(undo_future.GetCallback());
compose::mojom::ComposeStatePtr state = undo_future.Take();
EXPECT_EQ("second state", state->webui_state);
EXPECT_TRUE(state->response->undo_available);
base::test::TestFuture<compose::mojom::ComposeStatePtr> undo_future2;
page_handler()->Undo(undo_future2.GetCallback());
compose::mojom::ComposeStatePtr state2 = undo_future2.Take();
EXPECT_EQ("first state", state2->webui_state);
EXPECT_FALSE(state2->response->undo_available);
}
// Tests scenario: Undo returns state A, Compose, then undo again returns to
// state A.
TEST_F(ChromeComposeClientTest, TestUndoComposeThenUndoAgain) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
page_handler()->SaveWebUIState("first state");
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr response = compose_future.Take();
EXPECT_FALSE(response->undo_available) << "First Compose() response should "
"say undo is not available.";
page_handler()->SaveWebUIState("second state");
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
response = compose_future.Take();
EXPECT_TRUE(response->undo_available) << "Second Compose() response should "
"say undo is available.";
page_handler()->SaveWebUIState("wip web ui state");
base::test::TestFuture<compose::mojom::ComposeStatePtr> undo_future;
page_handler()->Undo(undo_future.GetCallback());
EXPECT_EQ("first state", undo_future.Take()->webui_state);
page_handler()->SaveWebUIState("third state");
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
response = compose_future.Take();
EXPECT_TRUE(response->undo_available) << "Third Compose() response should "
"say undo is available.";
base::test::TestFuture<compose::mojom::ComposeStatePtr> undo2_future;
page_handler()->Undo(undo2_future.GetCallback());
EXPECT_EQ("first state", undo2_future.Take()->webui_state);
}
// Tests that the corresponding callback is run when AcceptComposeResponse is
// called.
TEST_F(ChromeComposeClientTest, TestAcceptComposeResultCallback) {
base::test::TestFuture<const std::u16string&> accept_callback;
ShowDialogAndBindMojo(accept_callback.GetCallback());
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucumbers")));
})));
EXPECT_CALL(compose_dialog(), ResponseReceived(_));
// Before Compose is called AcceptComposeResult will return false.
base::test::TestFuture<bool> accept_future_1;
page_handler()->AcceptComposeResult(accept_future_1.GetCallback());
EXPECT_EQ(false, accept_future_1.Take());
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
base::test::TestFuture<bool> accept_future_2;
page_handler()->AcceptComposeResult(accept_future_2.GetCallback());
EXPECT_EQ(true, accept_future_2.Take());
// Check that the original callback from Autofill was called correctly.
EXPECT_EQ(u"Cucumbers", accept_callback.Take());
}
TEST_F(ChromeComposeClientTest, BugReportOpensCorrectURL) {
GURL bug_url("https://goto.google.com/ccbrfd");
ShowDialogAndBindMojo();
ui_test_utils::TabAddedWaiter tab_add_waiter(browser());
page_handler()->OpenBugReportingLink();
// Wait for the resulting new tab to be created.
tab_add_waiter.Wait();
// Check that the new foreground tab is opened.
EXPECT_EQ(2, browser()->tab_strip_model()->count());
EXPECT_EQ(1, browser()->tab_strip_model()->active_index());
// This test uses web_contents->GetController()->GetPendingEntry() as it only
// verifies that a navigation has started, regardless of whether it commits or
// not.
content::WebContents* new_tab_webcontents =
browser()->tab_strip_model()->GetWebContentsAt(1);
EXPECT_EQ(bug_url,
new_tab_webcontents->GetController().GetPendingEntry()->GetURL());
}
// TODO(crbug.com/400504728): Remove after ComposeProactiveNudge is launched.
TEST_F(ChromeComposeClientTest, LearnMoreLinkOpensCorrectURL) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{compose::features::kEnableCompose},
{compose::features::kEnableComposeProactiveNudge});
GURL learn_more_url("https://support.google.com/chrome?p=help_me_write");
ShowDialogAndBindMojo();
ui_test_utils::TabAddedWaiter tab_add_waiter(browser());
page_handler()->OpenComposeLearnMorePage();
// Wait for the resulting new tab to be created.
tab_add_waiter.Wait();
// Check that the new foreground tab is opened.
EXPECT_EQ(2, browser()->tab_strip_model()->count());
EXPECT_EQ(1, browser()->tab_strip_model()->active_index());
// This test uses web_contents->GetController()->GetPendingEntry() as it only
// verifies that a navigation has started, regardless of whether it commits or
// not.
content::WebContents* new_tab_webcontents =
browser()->tab_strip_model()->GetWebContentsAt(1);
EXPECT_EQ(learn_more_url,
new_tab_webcontents->GetController().GetPendingEntry()->GetURL());
}
TEST_F(ChromeComposeClientTest, SurveyLinkOpensCorrectURL) {
GURL survey_url("https://goto.google.com/ccfsfd");
ShowDialogAndBindMojo();
ui_test_utils::TabAddedWaiter tab_add_waiter(browser());
page_handler()->OpenFeedbackSurveyLink();
// Wait for the resulting new tab to be created.
tab_add_waiter.Wait();
// Check that the new foreground tab is opened.
EXPECT_EQ(2, browser()->tab_strip_model()->count());
EXPECT_EQ(1, browser()->tab_strip_model()->active_index());
// This test uses web_contents->GetController()->GetPendingEntry() as it only
// verifies that a navigation has started, regardless of whether it commits or
// not.
content::WebContents* new_tab_webcontents =
browser()->tab_strip_model()->GetWebContentsAt(1);
EXPECT_EQ(survey_url,
new_tab_webcontents->GetController().GetPendingEntry()->GetURL());
}
// Tests that all ComposeSessions are deleted on page navigation.
TEST_F(ChromeComposeClientTest, ResetClientOnNavigation) {
ShowDialogAndBindMojo();
page_handler()->SaveWebUIState("first state");
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
autofill::FormFieldData field_2;
field_2.set_renderer_id(autofill::FieldRendererId(2));
ShowDialogAndBindMojoWithFieldData(field_2);
// There should be two sessions.
EXPECT_EQ(2, client().GetSessionCountForTest());
// Navigate to a new page.
GURL next_page("http://example.com/a.html");
NavigateAndCommit(web_contents(), next_page);
// All sessions should be deleted.
EXPECT_EQ(0, client().GetSessionCountForTest());
}
// Tests that the dialog close button logs to the correct corresponding
// histograms.
TEST_F(ChromeComposeClientTest, CloseButtonHistogramTest) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
// Simulate three Compose requests - two from edits.
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr response = compose_future.Take();
page_handler()->Compose("", compose::mojom::InputMode::kPolish, true);
response = compose_future.Take();
page_handler()->Compose("", compose::mojom::InputMode::kPolish, true);
response = compose_future.Take();
// Show the dialog a second time.
ShowDialogAndBindMojo();
// Simulate two undos.
base::test::TestFuture<compose::mojom::ComposeStatePtr> undo_future;
page_handler()->Undo(undo_future.GetCallback());
compose::mojom::ComposeStatePtr state = undo_future.Take();
page_handler()->Undo(undo_future.GetCallback());
state = undo_future.Take();
client().CloseUI(compose::mojom::CloseReason::kCloseButton);
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.EndedSession.CloseButtonClicked"));
histograms().ExpectUniqueSample(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kCloseButtonPressed, 1);
// Expect that three total Compose calls were recorded.
histograms().ExpectUniqueSample(
compose::kComposeSessionComposeCount + std::string(".Ignored"), 3, 1);
histograms().ExpectUniqueSample("Compose.Server.Session.ComposeCount.Ignored",
3, 1);
// Expect that two of the Compose calls were from edits.
histograms().ExpectUniqueSample(
compose::kComposeSessionUpdateInputCount + std::string(".Ignored"), 2, 1);
histograms().ExpectUniqueSample(
"Compose.Server.Session.SubmitEditCount.Ignored", 2, 1);
// Expect that two undos were done.
histograms().ExpectUniqueSample(
compose::kComposeSessionUndoCount + std::string(".Ignored"), 2, 1);
histograms().ExpectUniqueSample("Compose.Server.Session.UndoCount.Ignored", 2,
1);
// Expect that the dialog was shown twice.
histograms().ExpectUniqueSample(
compose::kComposeSessionDialogShownCount + std::string(".Ignored"), 2, 1);
histograms().ExpectUniqueSample(
"Compose.Server.Session.DialogShownCount.Ignored", 2, 1);
// Check expected session duration metrics
histograms().ExpectTotalCount(
compose::kComposeSessionDuration + std::string(".FRE"), 0);
histograms().ExpectTotalCount(
compose::kComposeSessionDuration + std::string(".MSBB"), 0);
histograms().ExpectUniqueTimeSample(
compose::kComposeSessionDuration + std::string(".Ignored"),
base::ScopedMockElapsedTimersForTest::kMockElapsedTime, 1);
histograms().ExpectUniqueTimeSample(
"Compose.Server.Session.Duration.Ignored",
base::ScopedMockElapsedTimersForTest::kMockElapsedTime, 1);
histograms().ExpectUniqueSample(compose::kComposeSessionOverOneDay, 0, 1);
// Check the expected event count metrics.
std::vector<std::pair<compose::ComposeSessionEventTypes, int>> event_counts =
{
{compose::ComposeSessionEventTypes::kComposeDialogOpened, 1},
{compose::ComposeSessionEventTypes::kMainDialogShown, 1},
{compose::ComposeSessionEventTypes::kFREShown, 0},
{compose::ComposeSessionEventTypes::kCreateClicked, 1},
{compose::ComposeSessionEventTypes::kSuccessfulRequest, 1},
{compose::ComposeSessionEventTypes::kUpdateClicked, 1},
{compose::ComposeSessionEventTypes::kUndoClicked, 1},
{compose::ComposeSessionEventTypes::kAnyModifierUsed, 0},
{compose::ComposeSessionEventTypes::kFailedRequest, 0},
};
for (auto [event_type, count] : event_counts) {
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
event_type, count);
histograms().ExpectBucketCount("Compose.Server.Session.EventCounts",
event_type, count);
histograms().ExpectBucketCount("Compose.OnDevice.Session.EventCounts",
event_type, 0);
}
// No FRE related close reasons should have been recorded.
histograms().ExpectTotalCount(compose::kComposeFirstRunSessionCloseReason, 0);
// No MSBB related close reasons should have been recorded.
histograms().ExpectTotalCount(compose::kComposeMSBBSessionCloseReason, 0);
}
// Tests that the dialog close button logs to the correct corresponding
// histograms.
TEST_F(ChromeComposeClientTest, ExpiredSessionHistogramTest) {
compose::Config& config = compose::GetMutableConfigForTesting();
// ElapsedTimer in test will return an elapsed time of 1337ms by default.
// Set the session lifetime threshold to be shorter than this to simulate
// expiry.
config.session_max_allowed_lifetime = base::Seconds(1);
ShowDialogAndBindMojo();
// Show the dialog a second time - this ends the previous session if it is now
// expired.
ShowDialogAndBindMojo();
histograms().ExpectUniqueSample(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kExceededMaxDuration, 1);
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.EndedSession.EndedImplicitly"));
// Expect that the dialog was shown once.
histograms().ExpectUniqueSample(
compose::kComposeSessionDialogShownCount + std::string(".Ignored"), 1, 1);
// Check expected session duration metrics
histograms().ExpectTotalCount(
compose::kComposeSessionDuration + std::string(".FRE"), 0);
histograms().ExpectTotalCount(
compose::kComposeSessionDuration + std::string(".MSBB"), 0);
histograms().ExpectUniqueTimeSample(
compose::kComposeSessionDuration + std::string(".Ignored"),
base::ScopedMockElapsedTimersForTest::kMockElapsedTime, 1);
histograms().ExpectUniqueSample(compose::kComposeSessionOverOneDay, 0, 1);
// No FRE related close reasons should have been recorded.
histograms().ExpectTotalCount(compose::kComposeFirstRunSessionCloseReason, 0);
// No MSBB related close reasons should have been recorded.
histograms().ExpectTotalCount(compose::kComposeMSBBSessionCloseReason, 0);
client().CloseUI(compose::mojom::CloseReason::kCloseButton);
}
TEST_F(ChromeComposeClientTest, ExpiredSessionMSBBHistogramTest) {
SetPrefsForComposeMSBBState(false);
compose::Config& config = compose::GetMutableConfigForTesting();
// ElapsedTimer in test will return an elapsed time of 1337ms by default.
// Set the session lifetime threshold to be shorter than this to simulate
// expiry.
config.session_max_allowed_lifetime = base::Seconds(1);
ShowDialogAndBindMojo();
// Show the dialog a second time - this ends the previous session if it is now
// expired.
ShowDialogAndBindMojo();
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.EndedSession.EndedImplicitly"));
histograms().ExpectUniqueSample(
compose::kComposeMSBBSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::kExceededMaxDuration, 1);
histograms().ExpectUniqueSample(
compose::kComposeMSBBSessionDialogShownCount + std::string(".Ignored"),
1, // Expect that one total MSBB dialog was shown.
1);
}
TEST_F(ChromeComposeClientTest, ExpiredSessionFirstRunHistogramTest) {
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
false);
compose::Config& config = compose::GetMutableConfigForTesting();
// ElapsedTimer in test will return an elapsed time of 1337ms by default.
// Set the session lifetime threshold to be shorter than this to simulate
// expiry.
config.session_max_allowed_lifetime = base::Seconds(1);
ShowDialogAndBindMojo();
// Show the dialog a second time - this ends the previous session if it is now
// expired.
ShowDialogAndBindMojo();
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.EndedSession.EndedImplicitly"));
histograms().ExpectUniqueSample(
compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::kExceededMaxDuration, 1);
histograms().ExpectUniqueSample(
compose::kComposeFirstRunSessionDialogShownCount +
std::string(".Ignored"),
1, // Expect that one total FRE dialog was shown.
1);
}
TEST_F(ChromeComposeClientTest, ExpiredSessionBlocksSavedStateNudgeTest) {
compose::Config& config = compose::GetMutableConfigForTesting();
autofill::FormData form_data;
form_data.set_url(GetPageUrl());
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
const autofill::FormFieldData& selected_field_data =
test_api(form_data).field(0);
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged;
// Start a Compose session on selected field.
ShowDialogAndBindMojoWithFieldData(selected_field_data);
EXPECT_TRUE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
// ElapsedTimer in test will return an elapsed time of 1337ms by default.
// Set the session lifetime threshold to be shorter than this to simulate
// expiry.
config.session_max_allowed_lifetime = base::Seconds(1);
ShowDialogAndBindMojoWithFieldData(selected_field_data);
// By default the saved state nudge is shown.
EXPECT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
}
TEST_F(ChromeComposeClientTest, CloseButtonMSBBHistogramTest) {
SetPrefsForComposeMSBBState(false);
ShowDialogAndBindMojo();
client().CloseUI(compose::mojom::CloseReason::kMSBBCloseButton);
histograms().ExpectUniqueSample(
compose::kComposeMSBBSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::kCloseButtonPressed, 1);
histograms().ExpectUniqueSample(
compose::kComposeMSBBSessionDialogShownCount + std::string(".Ignored"),
1, // Expect that one total MSBB dialog was shown.
1);
histograms().ExpectTotalCount(compose::kComposeMSBBSessionCloseReason, 1);
// No FRE related close reasons should have been recorded.
histograms().ExpectTotalCount(compose::kComposeFirstRunSessionCloseReason, 0);
// Check expected session duration metrics
histograms().ExpectTotalCount(
compose::kComposeSessionDuration + std::string(".FRE"), 0);
histograms().ExpectUniqueTimeSample(
compose::kComposeSessionDuration + std::string(".MSBB"),
base::ScopedMockElapsedTimersForTest::kMockElapsedTime, 1);
histograms().ExpectTotalCount(
compose::kComposeSessionDuration + std::string(".Inserted"), 0);
histograms().ExpectUniqueSample(compose::kComposeSessionOverOneDay, 0, 1);
// Check the expected event count metrics.
std::vector<std::pair<compose::ComposeSessionEventTypes, int>> event_counts =
{
{compose::ComposeSessionEventTypes::kComposeDialogOpened, 1},
{compose::ComposeSessionEventTypes::kMainDialogShown, 0},
{compose::ComposeSessionEventTypes::kFREShown, 0},
{compose::ComposeSessionEventTypes::kMSBBShown, 1},
{compose::ComposeSessionEventTypes::kFREAccepted, 0},
{compose::ComposeSessionEventTypes::kMSBBEnabled, 0},
};
for (auto [event_type, count] : event_counts) {
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
event_type, count);
histograms().ExpectBucketCount("Compose.Server.Session.EventCounts",
event_type, 0);
histograms().ExpectBucketCount("Compose.OnDevice.Session.EventCounts",
event_type, 0);
}
}
TEST_F(ChromeComposeClientTest,
CloseButtonMSBBEnabledDuringSessionHistogramTest) {
SetPrefsForComposeMSBBState(false);
ShowDialogAndBindMojo();
SetPrefsForComposeMSBBState(true);
// Show the dialog a second time.
ShowDialogAndBindMojo();
client().CloseUI(compose::mojom::CloseReason::kCloseButton);
histograms().ExpectUniqueSample(
compose::kComposeSessionComposeCount + std::string(".Ignored"),
0, // Expect that zero total Compose calls were recorded.
1);
histograms().ExpectUniqueSample(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kCloseButtonPressed, 1);
histograms().ExpectUniqueSample(compose::kComposeMSBBSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::
kAckedOrAcceptedWithoutInsert,
1);
histograms().ExpectUniqueSample(
compose::kComposeMSBBSessionDialogShownCount + std::string(".Accepted"),
1, // Expect that the dialog was shown once.
1);
histograms().ExpectTotalCount(compose::kComposeMSBBSessionCloseReason, 1);
// No FRE related close reasons should have been recorded.
histograms().ExpectTotalCount(compose::kComposeFirstRunSessionCloseReason, 0);
// Check the expected event count metrics.
std::vector<std::pair<compose::ComposeSessionEventTypes, int>> event_counts =
{
{compose::ComposeSessionEventTypes::kComposeDialogOpened, 1},
{compose::ComposeSessionEventTypes::kMainDialogShown, 1},
{compose::ComposeSessionEventTypes::kFREShown, 0},
{compose::ComposeSessionEventTypes::kFREAccepted, 0},
{compose::ComposeSessionEventTypes::kMSBBShown, 1},
{compose::ComposeSessionEventTypes::kMSBBEnabled, 1},
{compose::ComposeSessionEventTypes::kInsertClicked, 0},
{compose::ComposeSessionEventTypes::kCloseClicked, 1},
};
for (auto [event_type, count] : event_counts) {
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
event_type, count);
histograms().ExpectBucketCount("Compose.Server.Session.EventCounts",
event_type, 0);
histograms().ExpectBucketCount("Compose.OnDevice.Session.EventCounts",
event_type, 0);
}
}
TEST_F(ChromeComposeClientTest, FirstRunCloseDialogHistogramTest) {
// Enable FRE and show the dialog.
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
false);
ShowDialogAndBindMojo();
client().CloseUI(compose::mojom::CloseReason::kFirstRunCloseButton);
// The FRE close reason should be |kCloseButtonPressed|.
histograms().ExpectUniqueSample(
compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::kCloseButtonPressed, 1);
// The main dialog close reason should be |kEndedAtFre|.
histograms().ExpectTotalCount(compose::kComposeSessionCloseReason, 1);
histograms().ExpectUniqueSample(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kEndedAtFre, 1);
// Expect that the dialog was shown once ending without FRE completed.
histograms().ExpectUniqueSample(
compose::kComposeFirstRunSessionDialogShownCount +
std::string(".Ignored"),
1, 1);
// Check expected session duration metrics.
histograms().ExpectUniqueTimeSample(
compose::kComposeSessionDuration + std::string(".FRE"),
base::ScopedMockElapsedTimersForTest::kMockElapsedTime, 1);
histograms().ExpectTotalCount(
compose::kComposeSessionDuration + std::string(".MSBB"), 0);
histograms().ExpectTotalCount(
compose::kComposeSessionDuration + std::string(".Ignored"), 0);
histograms().ExpectTotalCount("Compose.Server.Session.Duration.Ignored", 0);
histograms().ExpectUniqueSample(compose::kComposeSessionOverOneDay, 0, 1);
// Check the expected event count metrics.
std::vector<std::pair<compose::ComposeSessionEventTypes, int>> event_counts =
{
{compose::ComposeSessionEventTypes::kComposeDialogOpened, 1},
{compose::ComposeSessionEventTypes::kMainDialogShown, 0},
{compose::ComposeSessionEventTypes::kFREShown, 1},
{compose::ComposeSessionEventTypes::kFREAccepted, 0},
{compose::ComposeSessionEventTypes::kMSBBShown, 0},
{compose::ComposeSessionEventTypes::kMSBBEnabled, 0},
};
for (auto [event_type, count] : event_counts) {
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
event_type, count);
histograms().ExpectBucketCount("Compose.Server.Session.EventCounts",
event_type, 0);
histograms().ExpectBucketCount("Compose.OnDevice.Session.EventCounts",
event_type, 0);
}
// Show the FRE dialog and end the session by re-opening with selection.
ShowDialogAndBindMojo();
field_data().set_value(u"user selected text");
SetSelection(u"selected text");
ShowDialogAndBindMojo();
histograms().ExpectBucketCount(
compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::kReplacedWithNewSession, 1);
histograms().ExpectBucketCount(
compose::kComposeFirstRunSessionDialogShownCount +
std::string(".Ignored"),
1, // Expect that the dialog was shown once.
2);
// The main dialog should not be shown.
histograms().ExpectTotalCount(
compose::kComposeSessionDialogShownCount + std::string(".Ignored"), 0);
}
TEST_F(ChromeComposeClientTest, FirstRunThenMSBBCloseDialogHistogramTest) {
// Set both FRE and MSBB dialog states.
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
false);
SetPrefsForComposeMSBBState(false);
// Dialog should show at FRE state.
ShowDialogAndBindMojo();
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.DialogSeen.FirstRunDisclaimer"));
// After acknowledging the disclaimer, dialog should show the MSBB state.
client().CompleteFirstRun();
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.DialogSeen.FirstRunMSBB"));
// End the session by re-opening with selection.
field_data().set_value(u"user selected text");
SetSelection(u"selected text");
ShowDialogAndBindMojo();
histograms().ExpectUniqueSample(
compose::kComposeMSBBSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::kReplacedWithNewSession, 1);
histograms().ExpectBucketCount(compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::
kAckedOrAcceptedWithoutInsert,
1);
// Expect that the FRE dialog was shown+acked once.
histograms().ExpectBucketCount(
compose::kComposeFirstRunSessionDialogShownCount +
std::string(".Acknowledged"),
1, 1);
// Expect that the MSBB dialog was shown+ignored once.
histograms().ExpectBucketCount(
compose::kComposeMSBBSessionDialogShownCount + std::string(".Ignored"), 1,
1);
// The main dialog close reason should be |kAckedFreEndedAtMsbb|.
histograms().ExpectTotalCount(compose::kComposeSessionCloseReason, 1);
histograms().ExpectUniqueSample(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kAckedFreEndedAtMsbb, 1);
// The main dialog should not be shown.
histograms().ExpectTotalCount(
compose::kComposeSessionDialogShownCount + std::string(".Ignored"), 0);
}
TEST_F(ChromeComposeClientTest, MSBBCloseDialogHistogramTest) {
// Set MSBB dialog state to show.
SetPrefsForComposeMSBBState(false);
// Dialog should show at MSBB state (and not first run).
ShowDialogAndBindMojo();
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.DialogSeen.FirstRunMSBB"));
EXPECT_EQ(0, user_action_tester().GetActionCount(
"Compose.DialogSeen.FirstRunDisclaimer"));
// End the session by re-opening with selection.
field_data().set_value(u"user selected text");
SetSelection(u"selected text");
ShowDialogAndBindMojo();
histograms().ExpectUniqueSample(
compose::kComposeMSBBSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::kReplacedWithNewSession, 1);
histograms().ExpectTotalCount(compose::kComposeFirstRunSessionCloseReason, 0);
// Expect that the MSBB dialog was shown+ignored once.
histograms().ExpectBucketCount(
compose::kComposeMSBBSessionDialogShownCount + std::string(".Ignored"), 1,
1);
// The main dialog close reason should be |kEndedAtMsbb|.
histograms().ExpectTotalCount(compose::kComposeSessionCloseReason, 1);
histograms().ExpectUniqueSample(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kEndedAtMsbb, 1);
// The main dialog should not be shown.
histograms().ExpectTotalCount(
compose::kComposeSessionDialogShownCount + std::string(".Ignored"), 0);
}
TEST_F(ChromeComposeClientTest, FirstRunCompletedHistogramTest) {
// Enable FRE and show the dialog.
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
false);
ShowDialogAndBindMojo();
// Show the dialog a second time.
ShowDialogAndBindMojo();
// Complete FRE and close.
client().CompleteFirstRun();
client().CloseUI(compose::mojom::CloseReason::kCloseButton);
histograms().ExpectUniqueSample(compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::
kAckedOrAcceptedWithoutInsert,
1);
// Expect that the dialog was shown twice ending with FRE completed.
histograms().ExpectUniqueSample(
compose::kComposeFirstRunSessionDialogShownCount +
std::string(".Acknowledged"),
2, 1);
// After FRE is completed, a new set of metrics should be collected for the
// remainder of the session.
histograms().ExpectUniqueSample(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kCloseButtonPressed, 1);
histograms().ExpectUniqueSample(
compose::kComposeSessionDialogShownCount + std::string(".Ignored"),
1, // The dialog was only shown once after having proceeded past FRE.
1);
// Check the expected event count metrics.
std::vector<std::pair<compose::ComposeSessionEventTypes, int>> event_counts =
{
{compose::ComposeSessionEventTypes::kComposeDialogOpened, 1},
{compose::ComposeSessionEventTypes::kMainDialogShown, 1},
{compose::ComposeSessionEventTypes::kFREShown, 1},
{compose::ComposeSessionEventTypes::kFREAccepted, 1},
{compose::ComposeSessionEventTypes::kMSBBShown, 0},
{compose::ComposeSessionEventTypes::kMSBBEnabled, 0},
};
for (auto [event_type, count] : event_counts) {
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
event_type, count);
histograms().ExpectBucketCount("Compose.Server.Session.EventCounts",
event_type, 0);
histograms().ExpectBucketCount("Compose.OnDevice.Session.EventCounts",
event_type, 0);
}
}
TEST_F(ChromeComposeClientTest,
FirstRunCompletedThenSuggestionAcceptedHistogramTest) {
// Enable FRE and show the dialog.
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
false);
ShowDialogAndBindMojo();
// Complete FRE then close by inserting.
client().CompleteFirstRun();
client().CloseUI(compose::mojom::CloseReason::kInsertButton);
histograms().ExpectUniqueSample(
compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::kAckedOrAcceptedWithInsert,
1);
// Check the expected session event count metrics.
std::vector<std::pair<compose::ComposeSessionEventTypes, int>> event_counts =
{
{compose::ComposeSessionEventTypes::kComposeDialogOpened, 1},
{compose::ComposeSessionEventTypes::kMainDialogShown, 1},
{compose::ComposeSessionEventTypes::kFREShown, 1},
{compose::ComposeSessionEventTypes::kFREAccepted, 1},
{compose::ComposeSessionEventTypes::kMSBBShown, 0},
{compose::ComposeSessionEventTypes::kMSBBEnabled, 0},
{compose::ComposeSessionEventTypes::kStartedWithSelection, 0},
{compose::ComposeSessionEventTypes::kInsertClicked, 1},
};
for (auto [event_type, count] : event_counts) {
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
event_type, count);
histograms().ExpectBucketCount("Compose.Server.Session.EventCounts",
event_type, 0);
histograms().ExpectBucketCount("Compose.OnDevice.Session.EventCounts",
event_type, 0);
}
}
TEST_F(ChromeComposeClientTest, CompleteFirstRunTest) {
// Enable FRE and show the dialog.
PrefService* prefs = GetProfile()->GetPrefs();
prefs->SetBoolean(prefs::kPrefHasCompletedComposeFRE, false);
ShowDialogAndBindMojo();
client().CompleteFirstRun();
EXPECT_TRUE(prefs->GetBoolean(prefs::kPrefHasCompletedComposeFRE));
// Make sure the async calls complete before naviagating away.
FlushMojo();
// Navigate page away to upload session close metrics.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check the expected event count metrics.
std::vector<std::pair<compose::ComposeSessionEventTypes, int>> event_counts =
{
{compose::ComposeSessionEventTypes::kComposeDialogOpened, 1},
{compose::ComposeSessionEventTypes::kMainDialogShown, 1},
{compose::ComposeSessionEventTypes::kFREShown, 1},
{compose::ComposeSessionEventTypes::kMSBBShown, 0},
{compose::ComposeSessionEventTypes::kCreateClicked, 0},
};
for (auto [event_type, count] : event_counts) {
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
event_type, count);
histograms().ExpectBucketCount("Compose.Server.Session.EventCounts",
event_type, 0);
histograms().ExpectBucketCount("Compose.OnDevice.Session.EventCounts",
event_type, 0);
}
}
TEST_F(ChromeComposeClientTest,
AddSiteToNeverPromptListBlocksProactiveNudgeTest) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = true;
config.proactive_nudge_show_probability = 1.0;
config.proactive_nudge_field_per_navigation = false;
config.proactive_nudge_focus_delay = base::Microseconds(4);
config.proactive_nudge_segmentation = false;
PrefService* prefs = GetProfile()->GetPrefs();
auto test_url = GURL("http://foo");
auto test_origin = url::Origin::Create(test_url);
autofill::FormData form_data;
form_data.set_url(test_url);
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
autofill::FormFieldData& selected_field_data = test_api(form_data).field(0);
selected_field_data.set_origin(test_origin);
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged;
EXPECT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
task_environment()->FastForwardBy(config.proactive_nudge_focus_delay);
EXPECT_TRUE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
client().AddSiteToNeverPromptList(test_origin);
EXPECT_TRUE(prefs->GetDict(prefs::kProactiveNudgeDisabledSitesWithTime)
.Find(test_origin.Serialize()));
EXPECT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
NavigateAndCommitActiveTab(GURL("about:blank"));
histograms().ExpectBucketCount(
compose::kComposeProactiveNudgeCtr,
compose::ComposeNudgeCtrEvent::kUserDisabledSite, 1);
histograms().ExpectBucketCount(compose::kComposeProactiveNudgeCtr,
compose::ComposeNudgeCtrEvent::kNudgeDisplayed,
1);
histograms().ExpectTotalCount(compose::kComposeSelectionNudgeCtr, 0);
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kProactiveNudgeDisabledGloballyName,
ukm::builders::Compose_PageEvents::kProactiveNudgeDisabledForSiteName});
ASSERT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(ukm::builders::Compose_PageEvents::
kProactiveNudgeDisabledGloballyName,
0),
testing::Pair(ukm::builders::Compose_PageEvents::
kProactiveNudgeDisabledForSiteName,
1)));
}
TEST_F(ChromeComposeClientTest,
AddSiteToNeverPromptListBlocksSelectionNudgeTest) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = true;
config.proactive_nudge_field_per_navigation = false;
config.proactive_nudge_show_probability = 1.0;
config.proactive_nudge_focus_delay = base::Microseconds(4);
config.proactive_nudge_segmentation = false;
PrefService* prefs = GetProfile()->GetPrefs();
auto test_url = GURL("http://foo");
auto test_origin = url::Origin::Create(test_url);
autofill::FormData form_data;
form_data.set_url(test_url);
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
autofill::FormFieldData& selected_field_data = test_api(form_data).field(0);
selected_field_data.set_origin(test_origin);
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged;
EXPECT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
task_environment()->FastForwardBy(config.proactive_nudge_focus_delay);
// Set the most recent nudge to the selection nudge.
client().ShowProactiveNudge(form_data.global_id(),
selected_field_data.global_id(),
compose::ComposeEntryPoint::kSelectionNudge);
EXPECT_TRUE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
client().AddSiteToNeverPromptList(test_origin);
EXPECT_TRUE(prefs->GetDict(prefs::kProactiveNudgeDisabledSitesWithTime)
.Find(test_origin.Serialize()));
EXPECT_FALSE(client().ShouldTriggerPopup(form_data, selected_field_data,
trigger_source));
NavigateAndCommitActiveTab(GURL("about:blank"));
histograms().ExpectUniqueSample(
compose::kComposeSelectionNudgeCtr,
compose::ComposeNudgeCtrEvent::kUserDisabledSite, 1);
histograms().ExpectUniqueSample(
compose::kComposeProactiveNudgeCtr,
compose::ComposeNudgeCtrEvent::kNudgeDisplayed, 1);
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kProactiveNudgeDisabledGloballyName,
ukm::builders::Compose_PageEvents::kProactiveNudgeDisabledForSiteName});
ASSERT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(ukm::builders::Compose_PageEvents::
kProactiveNudgeDisabledGloballyName,
0),
testing::Pair(ukm::builders::Compose_PageEvents::
kProactiveNudgeDisabledForSiteName,
0)));
}
TEST_F(ChromeComposeClientTest, DisableComposeBlocksProactiveNudgeTest) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = true;
config.proactive_nudge_field_per_navigation = false;
config.proactive_nudge_show_probability = 1.0;
config.proactive_nudge_focus_delay = base::Microseconds(4);
config.proactive_nudge_segmentation = false;
PrefService* prefs = GetProfile()->GetPrefs();
EXPECT_TRUE(prefs->GetBoolean(prefs::kEnableProactiveNudge));
autofill::FormData form_data;
form_data.set_url(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
autofill::FormFieldData& field_data = test_api(form_data).field(0);
field_data.set_origin(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged;
EXPECT_FALSE(
client().ShouldTriggerPopup(form_data, field_data, trigger_source));
task_environment()->FastForwardBy(config.proactive_nudge_focus_delay);
EXPECT_TRUE(
client().ShouldTriggerPopup(form_data, field_data, trigger_source));
client().DisableProactiveNudge();
EXPECT_FALSE(prefs->GetBoolean(prefs::kEnableProactiveNudge));
EXPECT_FALSE(
client().ShouldTriggerPopup(form_data, field_data, trigger_source));
NavigateAndCommitActiveTab(GURL("about:blank"));
histograms().ExpectBucketCount(
compose::kComposeProactiveNudgeCtr,
compose::ComposeNudgeCtrEvent::kUserDisabledProactiveNudge, 1);
histograms().ExpectBucketCount(compose::kComposeProactiveNudgeCtr,
compose::ComposeNudgeCtrEvent::kNudgeDisplayed,
1);
histograms().ExpectTotalCount(compose::kComposeSelectionNudgeCtr, 0);
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kProactiveNudgeDisabledGloballyName,
ukm::builders::Compose_PageEvents::kProactiveNudgeDisabledForSiteName});
ASSERT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(ukm::builders::Compose_PageEvents::
kProactiveNudgeDisabledGloballyName,
1),
testing::Pair(ukm::builders::Compose_PageEvents::
kProactiveNudgeDisabledForSiteName,
0)));
}
TEST_F(ChromeComposeClientTest, DisableComposeBlocksSelectionNudgeTest) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = true;
config.proactive_nudge_field_per_navigation = false;
config.proactive_nudge_show_probability = 1.0;
config.proactive_nudge_focus_delay = base::Microseconds(4);
config.proactive_nudge_segmentation = false;
PrefService* prefs = GetProfile()->GetPrefs();
EXPECT_TRUE(prefs->GetBoolean(prefs::kEnableProactiveNudge));
autofill::FormData form_data;
form_data.set_url(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
autofill::FormFieldData& field_data = test_api(form_data).field(0);
field_data.set_origin(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
const autofill::AutofillSuggestionTriggerSource trigger_source =
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged;
EXPECT_FALSE(
client().ShouldTriggerPopup(form_data, field_data, trigger_source));
task_environment()->FastForwardBy(config.proactive_nudge_focus_delay);
// Set the most recent nudge to the selection nudge.
client().ShowProactiveNudge(form_data.global_id(), field_data.global_id(),
compose::ComposeEntryPoint::kSelectionNudge);
EXPECT_TRUE(
client().ShouldTriggerPopup(form_data, field_data, trigger_source));
client().DisableProactiveNudge();
EXPECT_FALSE(prefs->GetBoolean(prefs::kEnableProactiveNudge));
EXPECT_FALSE(
client().ShouldTriggerPopup(form_data, field_data, trigger_source));
NavigateAndCommitActiveTab(GURL("about:blank"));
histograms().ExpectUniqueSample(
compose::kComposeSelectionNudgeCtr,
compose::ComposeNudgeCtrEvent::kUserDisabledProactiveNudge, 1);
histograms().ExpectUniqueSample(
compose::kComposeProactiveNudgeCtr,
compose::ComposeNudgeCtrEvent::kNudgeDisplayed, 1);
auto ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_PageEvents::kEntryName,
{ukm::builders::Compose_PageEvents::kProactiveNudgeDisabledGloballyName,
ukm::builders::Compose_PageEvents::kProactiveNudgeDisabledForSiteName});
ASSERT_EQ(ukm_entries.size(), 1UL);
EXPECT_THAT(ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(ukm::builders::Compose_PageEvents::
kProactiveNudgeDisabledGloballyName,
0),
testing::Pair(ukm::builders::Compose_PageEvents::
kProactiveNudgeDisabledForSiteName,
0)));
}
TEST_F(ChromeComposeClientTest, TextFieldChangeThresholdHidesProactiveNudge) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = true;
config.proactive_nudge_show_probability = 1.0;
config.proactive_nudge_segmentation = false;
client().field_change_observer_.SetSkipSuggestionTypeForTest(true);
autofill::FormData form_data;
form_data.set_url(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
form_data.set_fields({autofill::test::CreateTestFormField(
"label0", "name0", "value0", autofill::FormControlType::kTextArea)});
// Simulate an Autofill popup being shown.
autofill::AutofillClient::PopupOpenArgs args;
args.suggestions = {
autofill::Suggestion(autofill::SuggestionType::kComposeProactiveNudge)};
autofill_client()->ShowAutofillSuggestions(args, /*delegate=*/nullptr);
EXPECT_TRUE(autofill_client()->IsShowingAutofillPopup());
// Simulate field change events up to limit specified by config.
std::u16string text_value = u"a";
unsigned int max = config.nudge_field_change_event_max;
for (size_t i = 1; i < max; i++) {
client().field_change_observer_.OnAfterTextFieldValueChanged(
*autofill_manager(), form_data.global_id(),
form_data.fields()[0].global_id(), text_value);
EXPECT_EQ(
i,
client().field_change_observer_.text_field_value_change_event_count_);
text_value = text_value + u"a";
}
// Reaching the event threshold resets the event count and hides the Autofill
// popup.
client().field_change_observer_.OnAfterTextFieldValueChanged(
*autofill_manager(), form_data.global_id(),
form_data.fields()[0].global_id(), text_value);
EXPECT_EQ(
0U, client().field_change_observer_.text_field_value_change_event_count_);
EXPECT_FALSE(autofill_client()->IsShowingAutofillPopup());
}
TEST_F(ChromeComposeClientTest, AcceptSuggestionHistogramTest) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
// Simulate three compose requests - two from edits.
page_handler()->Compose("", compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr response = compose_future.Take();
page_handler()->Compose("", compose::mojom::InputMode::kPolish, true);
response = compose_future.Take();
page_handler()->Compose("", compose::mojom::InputMode::kPolish, true);
response = compose_future.Take();
// Show the dialog a second time.
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeStatePtr> undo_future;
page_handler()->Undo(undo_future.GetCallback());
compose::mojom::ComposeStatePtr state = undo_future.Take();
// Show the dialog a third time.
ShowDialogAndBindMojo();
client().CloseUI(compose::mojom::CloseReason::kInsertButton);
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.EndedSession.InsertButtonClicked"));
histograms().ExpectUniqueSample(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kInsertedResponse, 1);
histograms().ExpectUniqueSample(
compose::kComposeSessionComposeCount + std::string(".Accepted"),
3, // Expect that three Compose calls were recorded.
1);
histograms().ExpectUniqueSample(
compose::kComposeSessionUpdateInputCount + std::string(".Accepted"),
2, // Expect that two of the Compose calls were from edits.
1);
histograms().ExpectUniqueSample(
compose::kComposeSessionUndoCount + std::string(".Accepted"),
1, // Expect that one undo was done.
1);
histograms().ExpectUniqueSample(
compose::kComposeSessionDialogShownCount + std::string(".Accepted"),
3, // Expect that the dialog was shown three times.
1);
histograms().ExpectUniqueSample(
"Compose.Server.Session.DialogShownCount.Accepted",
3, // Expect that the dialog was shown three times.
1);
// Check expected session duration metrics.
histograms().ExpectTotalCount(
compose::kComposeSessionDuration + std::string(".FRE"), 0);
histograms().ExpectTotalCount(
compose::kComposeSessionDuration + std::string(".MSBB"), 0);
histograms().ExpectUniqueTimeSample(
compose::kComposeSessionDuration + std::string(".Inserted"),
base::ScopedMockElapsedTimersForTest::kMockElapsedTime, 1);
histograms().ExpectUniqueTimeSample(
"Compose.Server.Session.Duration.Inserted",
base::ScopedMockElapsedTimersForTest::kMockElapsedTime, 1);
histograms().ExpectUniqueSample(compose::kComposeSessionOverOneDay, 0, 1);
}
TEST_F(ChromeComposeClientTest, LoseFocusHistogramTest) {
ShowDialogAndBindMojo();
// Dismiss dialog by losing focus by navigating.
GURL next_page("http://example.com/a.html");
NavigateAndCommit(web_contents(), next_page);
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.EndedSession.EndedImplicitly"));
histograms().ExpectUniqueSample(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kAbandoned, 1);
}
TEST_F(ChromeComposeClientTest, LoseFocusFirstRunHistogramTest) {
// Enable FRE and show the dialog.
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
false);
ShowDialogAndBindMojo();
// Dismiss dialog by losing focus by navigating.
GURL next_page("http://example.com/a.html");
NavigateAndCommit(web_contents(), next_page);
histograms().ExpectUniqueSample(
compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFreOrMsbbSessionCloseReason::kAbandoned, 1);
}
TEST_F(ChromeComposeClientTest, ComposeDialogStatesSeenUserActionsTest) {
// Set both FRE and MSBB dialog states to show and check that appropriate
// user actions are logged when moving through all states in a single session.
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
false);
SetPrefsForComposeMSBBState(false);
EXPECT_EQ(0, user_action_tester().GetActionCount(
"Compose.DialogSeen.FirstRunDisclaimer"));
EXPECT_EQ(0, user_action_tester().GetActionCount(
"Compose.DialogSeen.FirstRunMSBB"));
EXPECT_EQ(
0, user_action_tester().GetActionCount("Compose.DialogSeen.MainDialog"));
// Dialog should show at FRE state.
ShowDialogAndBindMojo();
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.DialogSeen.FirstRunDisclaimer"));
// After acknowledging the disclaimer, dialog should show the MSBB state.
client().CompleteFirstRun();
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.DialogSeen.FirstRunMSBB"));
// After updating the MSBB setting, only the next open of the dialog should
// record a dialog seen action.
SetPrefsForComposeMSBBState(true);
ShowDialogAndBindMojo();
EXPECT_EQ(
1, user_action_tester().GetActionCount("Compose.DialogSeen.MainDialog"));
// Show dialog again.
ShowDialogAndBindMojo();
EXPECT_EQ(
1, user_action_tester().GetActionCount("Compose.DialogSeen.MainDialog"));
client().CloseUI(compose::mojom::CloseReason::kCloseButton);
// Check user actions for new session opened at MSBB state.
SetPrefsForComposeMSBBState(false);
ShowDialogAndBindMojo();
EXPECT_EQ(2, user_action_tester().GetActionCount(
"Compose.DialogSeen.FirstRunMSBB"));
client().CloseUI(compose::mojom::CloseReason::kMSBBCloseButton);
// Check user actions for new session opened at main dialog state.
SetPrefsForComposeMSBBState(true);
ShowDialogAndBindMojo();
EXPECT_EQ(
2, user_action_tester().GetActionCount("Compose.DialogSeen.MainDialog"));
client().CloseUI(compose::mojom::CloseReason::kCloseButton);
// Check user actions for session opened at FRE state and progressing directly
// to main dialog state.
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
false);
ShowDialogAndBindMojo();
EXPECT_EQ(2, user_action_tester().GetActionCount(
"Compose.DialogSeen.FirstRunDisclaimer"));
// After acknowledging the disclaimer, dialog should show the main state.
client().CompleteFirstRun();
EXPECT_EQ(
3, user_action_tester().GetActionCount("Compose.DialogSeen.MainDialog"));
}
TEST_F(ChromeComposeClientTest, TestAutoCompose) {
EnableAutoCompose();
base::test::TestFuture<void> execute_model_future;
// Make model execution hang.
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(base::test::RunOnceClosure(execute_model_future.GetCallback()));
std::u16string selected_text = u"ŧëśŧĩňĝ âľpħâ ƅřâɤō ĉħâŗľĩë";
std::string selected_text_utf8 = base::UTF16ToUTF8(selected_text);
SetSelection(selected_text);
ShowDialogAndBindMojo();
FlushMojo();
// Check that the UTF8 byte length has zero counts.
histograms().ExpectBucketCount(compose::kComposeDialogSelectionLength,
base::UTF16ToUTF8(selected_text).size(), 0);
// Check that the number of UTF8 code points has one count.
histograms().ExpectBucketCount(
compose::kComposeDialogSelectionLength,
base::CountUnicodeCharacters(selected_text_utf8).value(), 1);
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_TRUE(result->compose_state->has_pending_request);
EXPECT_TRUE(execute_model_future.Wait());
// Check that opening from the context menu with an empty selection resumes
// without autocompose.
SetSelection(u"");
// Would crash if Compose is called again since we expect ExecuteModel to run
// just once.
ShowDialogAndBindMojo();
FlushMojo();
// Check opening from the saved state menu with a selection resumes without
// autocompose.
SetSelection(u"Some new selected text");
// Would crash if Compose is called again since we expect ExecuteModel to run
// just once.
ShowDialogAndBindMojoWithFieldData(
field_data(), base::NullCallback(),
autofill::AutofillComposeDelegate::UiEntryPoint::kAutofillPopup);
}
TEST_F(ChromeComposeClientTest, TestAutoComposeTooLong) {
EnableAutoCompose();
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(0);
std::u16string words(compose::GetComposeConfig().input_max_chars - 3, u'a');
words += u" b c";
SetSelection(words);
ShowDialogAndBindMojo();
histograms().ExpectUniqueSample(compose::kComposeDialogSelectionLength,
base::UTF16ToUTF8(words).size(), 1);
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_FALSE(result->compose_state->has_pending_request);
}
TEST_F(ChromeComposeClientTest, TestAutoComposeTooFewWords) {
EnableAutoCompose();
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(0);
std::u16string words(40, u'a');
words += u" b";
SetSelection(words);
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_FALSE(result->compose_state->has_pending_request);
}
TEST_F(ChromeComposeClientTest, TestAutoComposeTooManyWords) {
EnableAutoCompose();
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(0);
std::u16string words = u"b";
// Words should be the max plus 1.
for (uint32_t i = 0; i < compose::GetComposeConfig().input_max_words; ++i) {
words += u" b";
}
SetSelection(words);
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_FALSE(result->compose_state->has_pending_request);
}
TEST_F(ChromeComposeClientTest, TestAutoComposeDisabled) {
// Auto compose is disabled by default.
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(0);
SetSelection(u"testing alpha bravo charlie");
ShowDialogAndBindMojo();
}
TEST_F(ChromeComposeClientTest, TestNoAutoComposeWithPopup) {
EnableAutoCompose();
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(0);
SetSelection(u"a"); // Too short to cause auto compose.
ShowDialogAndBindMojo();
SetSelection(u"testing alpha bravo charlie");
// Show again.
ShowDialogAndBindMojoWithFieldData(
field_data(), base::NullCallback(),
autofill::AutofillComposeDelegate::UiEntryPoint::kAutofillPopup);
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_FALSE(result->compose_state->has_pending_request);
}
TEST_F(ChromeComposeClientTest, TestAutoComposeWithRepeatedRightClick) {
EnableAutoCompose();
base::test::TestFuture<void> execute_model_future;
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(base::test::RunOnceClosure(execute_model_future.GetCallback()));
SetSelection(u"a"); // Too short to cause auto compose.
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_FALSE(result->compose_state->has_pending_request);
std::u16string selection = u"testing alpha bravo charlie";
SetSelection(selection);
// Show again.
ShowDialogAndBindMojo();
EXPECT_TRUE(execute_model_future.Wait());
page_handler()->RequestInitialState(open_test_future.GetCallback());
result = open_test_future.Take();
EXPECT_TRUE(result->compose_state->has_pending_request);
EXPECT_EQ(base::UTF16ToUTF8(selection), result->initial_input);
}
TEST_F(ChromeComposeClientTest, TestNoAutoComposeBeforeFirstRun) {
EnableAutoCompose();
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(0);
// Enable FRE and show the dialog.
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
false);
// Valid selection for auto compose to use.
std::u16string selection = u"testing alpha bravo charlie";
SetSelection(selection);
ShowDialogAndBindMojo();
// Without FRE completion auto compose should not execute.
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_FALSE(result->compose_state->has_pending_request);
}
// Tests that quality logs are uploaded when a new valid response clears forward
// state and when the session is destroyed, and that those logs have the
// expected session IDs attached.
TEST_F(ChromeComposeClientTest, TestComposeQualitySessionId) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(3);
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
page_handler()->Compose("a user typed one",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Wait());
// Reset future for second compose call.
compose_future.Clear();
page_handler()->Compose("a user typed two",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Wait());
// Reset future for third compose call.
compose_future.Clear();
base::test::TestFuture<compose::mojom::ComposeStatePtr> undo_future;
// Undo reverts client to the first saved state in the history, with one
// forward state resulting from the second compose.
page_handler()->Undo(undo_future.GetCallback());
EXPECT_TRUE(undo_future.Wait());
// Third compose should clear the forward state from the second compose and
// upload its corresponding quality logs.
page_handler()->Compose("a user typed three",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Wait());
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(1u, uploaded_logs().size());
const auto& session_id = uploaded_logs()[0]->compose().quality().session_id();
EXPECT_EQ(kSessionIdHigh, session_id.high());
EXPECT_EQ(kSessionIdLow, session_id.low());
// Wait for two log uploads.
log_uploaded_signal.Clear();
logs_uploader().WaitForLogUpload(
log_uploaded_signal.GetCallback().Then(base::BindLambdaForTesting([&]() {
EXPECT_TRUE(log_uploaded_signal.WaitAndClear());
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
})));
client_page_handler()->CloseUI(compose::mojom::CloseReason::kInsertButton);
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(3u, uploaded_logs().size());
const auto& session_id2 =
uploaded_logs()[1]->compose().quality().session_id();
EXPECT_EQ(kSessionIdHigh, session_id2.high());
EXPECT_EQ(kSessionIdLow, session_id2.low());
const auto& session_id3 =
uploaded_logs()[2]->compose().quality().session_id();
EXPECT_EQ(kSessionIdHigh, session_id3.high());
EXPECT_EQ(kSessionIdLow, session_id3.low());
EXPECT_EQ(
optimization_guide::proto::FinalModelStatus::FINAL_MODEL_STATUS_SUCCESS,
uploaded_logs()[1]->compose().quality().final_model_status());
}
TEST_F(ChromeComposeClientTest, TestComposeQualityLoggedOnSubsequentError) {
ShowDialogAndBindMojo();
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillRepeatedly(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(
OptimizationGuideModelStreamingExecutionResult(
base::unexpected(
OptimizationGuideModelExecutionError::
FromModelExecutionError(
OptimizationGuideModelExecutionError::
ModelExecutionError::kGenericFailure)),
/*provided_by_on_device=*/false,
std::make_unique<ModelExecutionInfo>()));
})));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillRepeatedly(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
compose_future.SetValue(std::move(response));
}));
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr compose_result = compose_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kServerError,
compose_result->status);
page_handler()->Compose("a user typed that",
compose::mojom::InputMode::kPolish, false);
compose_result = compose_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kServerError,
compose_result->status);
// Ensure that a quality log is emitted after a second compose error.
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(1u, uploaded_logs().size());
EXPECT_EQ(kSessionIdLow,
uploaded_logs()[0]->compose().quality().session_id().low());
// Close UI to submit remaining quality logs.
log_uploaded_signal.Clear();
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(2u, uploaded_logs().size());
EXPECT_EQ(
base::ScopedMockElapsedTimersForTest::kMockElapsedTime.InMilliseconds(),
uploaded_logs()[1]->compose().quality().request_latency_ms());
// Check that histogram was sent for Compose State removed from undo stack.
histograms().ExpectBucketCount("Compose.Server.Request.Feedback",
compose::ComposeRequestFeedback::kNoFeedback,
0);
histograms().ExpectBucketCount("Compose.Server.Request.Feedback",
compose::ComposeRequestFeedback::kRequestError,
2);
}
// Tests that quality logs are uploaded when a new valid response clears forward
// state and when the session is destroyed, and that those logs have expected
// latency data attached.
TEST_F(ChromeComposeClientTest, TestComposeQualityLatency) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(3);
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
page_handler()->Compose("a user typed one",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Wait());
// Reset future for second compose call.
compose_future.Clear();
page_handler()->Compose("a user typed two",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Wait());
// Reset future for third compose call.
compose_future.Clear();
base::test::TestFuture<compose::mojom::ComposeStatePtr> undo_future;
// Undo reverts client to the first saved state in the history, with one
// forward state resulting from the second compose.
page_handler()->Undo(undo_future.GetCallback());
EXPECT_TRUE(undo_future.Wait());
// Third compose should clear the forward state from the second compose and
// upload its corresponding quality logs.
page_handler()->Compose("a user typed three",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Wait());
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(1u, uploaded_logs().size());
EXPECT_EQ(
base::ScopedMockElapsedTimersForTest::kMockElapsedTime.InMilliseconds(),
uploaded_logs()[0]->compose().quality().request_latency_ms());
// Close UI should result in upload of quality logs for the two responses left
// in the state history.
log_uploaded_signal.Clear();
logs_uploader().WaitForLogUpload(
log_uploaded_signal.GetCallback().Then(base::BindLambdaForTesting([&]() {
EXPECT_TRUE(log_uploaded_signal.WaitAndClear());
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
})));
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(3u, uploaded_logs().size());
EXPECT_EQ(
base::ScopedMockElapsedTimersForTest::kMockElapsedTime.InMilliseconds(),
uploaded_logs()[1]->compose().quality().request_latency_ms());
EXPECT_EQ(
base::ScopedMockElapsedTimersForTest::kMockElapsedTime.InMilliseconds(),
uploaded_logs()[2]->compose().quality().request_latency_ms());
}
TEST_F(ChromeComposeClientTest,
TestComposeQualityOnlyOneLogEntryAbandonedOnClose) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(2);
// Wait for two log uploads.
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(
log_uploaded_signal.GetCallback().Then(base::BindLambdaForTesting([&]() {
EXPECT_TRUE(log_uploaded_signal.WaitAndClear());
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
})));
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Wait()); // Reset future for second compose call.
compose_future.Clear();
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Wait());
// Close UI to submit remaining quality logs.
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(2u, uploaded_logs().size());
EXPECT_EQ(optimization_guide::proto::FinalStatus::STATUS_ABANDONED,
uploaded_logs()[0]->compose().quality().final_status());
EXPECT_EQ(optimization_guide::proto::FinalStatus::STATUS_UNSPECIFIED,
uploaded_logs()[1]->compose().quality().final_status());
}
TEST_F(ChromeComposeClientTest, TestComposeQualityNewSessionWithSelectedText) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(2);
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Take()); // Reset future for second compose call.
// Start a new session with selected text.
field_data().set_value(u"user selected text");
SetSelection(u"selected text");
ShowDialogAndBindMojo();
// Get quality result from the abandoned session.
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(1u, uploaded_logs().size());
EXPECT_EQ(optimization_guide::proto::FinalStatus::STATUS_ABANDONED,
uploaded_logs()[0]->compose().quality().final_status());
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Take());
// Close UI to submit remaining quality logs.
log_uploaded_signal.Clear();
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(2u, uploaded_logs().size());
EXPECT_EQ(optimization_guide::proto::FinalStatus::STATUS_ABANDONED,
uploaded_logs()[1]->compose().quality().final_status());
}
TEST_F(ChromeComposeClientTest, TestComposeQualityFinishedWithoutInsert) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
EXPECT_CALL(session(), ExecuteModel(_, _));
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Take()); // Reset future for second compose call.
// Navigate to a new page.
GURL next_page("http://example.com/a.html");
NavigateAndCommit(web_contents(), next_page);
// Get quality result from the abandoned session.
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(1u, uploaded_logs().size());
EXPECT_EQ(
optimization_guide::proto::FinalStatus::STATUS_FINISHED_WITHOUT_INSERT,
uploaded_logs()[0]->compose().quality().final_status());
}
TEST_F(ChromeComposeClientTest, TestComposeQualityFeedbackPositive) {
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(1);
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
ShowDialogAndBindMojo();
client().GetSessionForActiveComposeField()->SetSkipFeedbackUiForTesting(true);
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
ASSERT_TRUE(compose_future.Take());
page_handler()->SetUserFeedback(
compose::mojom::UserFeedback::kUserFeedbackPositive);
// Close UI to submit remaining quality logs.
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
// Get quality logs sent for the Compose Request.
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(1u, uploaded_logs().size());
EXPECT_EQ(optimization_guide::proto::UserFeedback::USER_FEEDBACK_THUMBS_UP,
uploaded_logs()[0]->compose().quality().user_feedback());
// Check that the histogram was sent for request feedback.
histograms().ExpectUniqueSample(
"Compose.Server.Request.Feedback",
compose::ComposeRequestFeedback::kPositiveFeedback, 1);
}
TEST_F(ChromeComposeClientTest, TestComposeQualityFeedbackNegative) {
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(1);
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
ShowDialogAndBindMojo();
client().GetSessionForActiveComposeField()->SetSkipFeedbackUiForTesting(true);
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
ASSERT_TRUE(compose_future.Take());
page_handler()->SetUserFeedback(
compose::mojom::UserFeedback::kUserFeedbackNegative);
// Close UI to submit remaining quality logs.
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
// Get quality logs sent for the Compose Request.
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(1u, uploaded_logs().size());
EXPECT_EQ(optimization_guide::proto::UserFeedback::USER_FEEDBACK_THUMBS_DOWN,
uploaded_logs()[0]->compose().quality().user_feedback());
EXPECT_EQ(
optimization_guide::proto::FinalModelStatus::FINAL_MODEL_STATUS_FAILURE,
uploaded_logs()[0]->compose().quality().final_model_status());
// Check that the histogram was sent for request feedback.
histograms().ExpectUniqueSample(
"Compose.Server.Request.Feedback",
compose::ComposeRequestFeedback::kNegativeFeedback, 1);
}
TEST_F(ChromeComposeClientTest, TestComposeQualityWasEdited) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(2);
// Wait for two log uploads.
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(
log_uploaded_signal.GetCallback().Then(base::BindLambdaForTesting([&]() {
EXPECT_TRUE(log_uploaded_signal.WaitAndClear());
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
})));
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
EXPECT_TRUE(compose_future.Wait()); // Reset future for second compose call.
compose_future.Clear();
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, true);
EXPECT_TRUE(compose_future.Wait());
// Close UI to submit remaining quality logs.
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
EXPECT_TRUE(log_uploaded_signal.Wait());
ASSERT_EQ(2u, uploaded_logs().size());
EXPECT_TRUE(uploaded_logs()[0]->compose().quality().was_generated_via_edit());
EXPECT_FALSE(
uploaded_logs()[1]->compose().quality().was_generated_via_edit());
EXPECT_EQ(optimization_guide::proto::FinalStatus::STATUS_UNSPECIFIED,
uploaded_logs()[1]->compose().quality().final_status());
histograms().ExpectBucketCount(
compose::kComposeRequestReason,
compose::ComposeRequestReason::kFirstRequestPolishMode, 1);
histograms().ExpectBucketCount(
"Compose.Server.Request.Reason",
compose::ComposeRequestReason::kFirstRequestPolishMode, 1);
histograms().ExpectBucketCount(compose::kComposeRequestReason,
compose::ComposeRequestReason::kUpdateRequest,
1);
histograms().ExpectBucketCount("Compose.Server.Request.Reason",
compose::ComposeRequestReason::kUpdateRequest,
1);
// Check that the histogram was sent for request feedback.
histograms().ExpectUniqueSample("Compose.Server.Request.Feedback",
compose::ComposeRequestFeedback::kNoFeedback,
2);
}
TEST_F(ChromeComposeClientTest, TestRegenerate) {
ShowDialogAndBindMojo();
std::string user_input = "a user typed this";
auto matcher = EqualsProto(ComposeRequest(
user_input,
optimization_guide::proto::ComposeUpfrontInputMode::COMPOSE_POLISH_MODE));
EXPECT_CALL(session(), ExecuteModel(matcher, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucumbers")));
})));
auto regen_matcher =
EqualsProto(RegenerateRequest(/*previous_response=*/"Cucumbers"));
EXPECT_CALL(session(), ExecuteModel(regen_matcher, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Tomatoes")));
})));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillRepeatedly(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
page_handler()->Compose(user_input, compose::mojom::InputMode::kPolish,
false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
EXPECT_EQ("Cucumbers", result->result);
page_handler()->Rewrite(compose::mojom::StyleModifier::kRetry);
result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
EXPECT_EQ("Tomatoes", result->result);
histograms().ExpectBucketCount(compose::kComposeRequestReason,
compose::ComposeRequestReason::kRetryRequest,
1);
histograms().ExpectBucketCount("Compose.Server.Request.Reason",
compose::ComposeRequestReason::kRetryRequest,
1);
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
// Make sure the async call to CloseUI completes before navigating away.
FlushMojo();
// Check Compose Session Event Counts.
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kRetryClicked, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kCloseClicked, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kAnyModifierUsed, 0);
// Navigate page away to upload UKM metrics to the collector.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check session level UKM metrics.
auto session_ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_SessionProgress::kEntryName,
{ukm::builders::Compose_SessionProgress::kRegenerateCountName});
EXPECT_EQ(session_ukm_entries.size(), 1UL);
EXPECT_THAT(
session_ukm_entries[0].metrics,
testing::UnorderedElementsAre(testing::Pair(
ukm::builders::Compose_SessionProgress::kRegenerateCountName, 1)));
}
TEST_F(ChromeComposeClientTest, TestToneChange) {
ShowDialogAndBindMojo();
std::string user_input = "a user typed this";
auto compose_matcher = EqualsProto(ComposeRequest(
user_input,
optimization_guide::proto::ComposeUpfrontInputMode::COMPOSE_POLISH_MODE));
EXPECT_CALL(session(), ExecuteModel(compose_matcher, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucumbers")));
})));
// Rewrite with Formal.
optimization_guide::proto::ComposeRequest request;
request.mutable_rewrite_params()->set_previous_response("Cucumbers");
request.mutable_rewrite_params()->set_tone(
optimization_guide::proto::ComposeTone::COMPOSE_FORMAL);
auto rewrite_matcher = EqualsProto(request);
EXPECT_CALL(session(), ExecuteModel(rewrite_matcher, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Tomatoes")));
})));
// Rewrite with Casual.
request.mutable_rewrite_params()->set_previous_response("Tomatoes");
request.mutable_rewrite_params()->set_tone(
optimization_guide::proto::ComposeTone::COMPOSE_INFORMAL);
auto rewrite_matcher_informal = EqualsProto(request);
EXPECT_CALL(session(), ExecuteModel(rewrite_matcher_informal, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Potatoes")));
})));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillRepeatedly(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
page_handler()->Compose(user_input, compose::mojom::InputMode::kPolish,
false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
EXPECT_EQ("Cucumbers", result->result);
page_handler()->Rewrite(compose::mojom::StyleModifier::kFormal);
result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
EXPECT_EQ("Tomatoes", result->result);
histograms().ExpectBucketCount(
compose::kComposeRequestReason,
compose::ComposeRequestReason::kToneFormalRequest, 1);
histograms().ExpectBucketCount(
"Compose.Server.Request.Reason",
compose::ComposeRequestReason::kToneFormalRequest, 1);
page_handler()->Rewrite(compose::mojom::StyleModifier::kCasual);
result = test_future.Take();
histograms().ExpectBucketCount(
compose::kComposeRequestReason,
compose::ComposeRequestReason::kToneCasualRequest, 1);
histograms().ExpectBucketCount(
"Compose.Server.Request.Reason",
compose::ComposeRequestReason::kToneCasualRequest, 1);
// Make sure the async call to CloseUI completes before navigating away.
FlushMojo();
// Navigate page away to upload UKM metrics to the collector.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check Compose Session Event Counts.
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kFormalClicked, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kCasualClicked, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kElaborateClicked, 0);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kShortenClicked, 0);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kAnyModifierUsed, 1);
// Check session level UKM metrics.
auto session_ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_SessionProgress::kEntryName,
{ukm::builders::Compose_SessionProgress::kCasualCountName,
ukm::builders::Compose_SessionProgress::kFormalCountName});
EXPECT_EQ(session_ukm_entries.size(), 1UL);
EXPECT_THAT(
session_ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(
ukm::builders::Compose_SessionProgress::kCasualCountName, 1),
testing::Pair(
ukm::builders::Compose_SessionProgress::kFormalCountName, 1)));
}
TEST_F(ChromeComposeClientTest, TestLengthChange) {
ShowDialogAndBindMojo();
std::string user_input = "a user typed this";
auto compose_matcher = EqualsProto(ComposeRequest(
user_input,
optimization_guide::proto::ComposeUpfrontInputMode::COMPOSE_POLISH_MODE));
EXPECT_CALL(session(), ExecuteModel(compose_matcher, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Cucumbers")));
})));
// Rewrite with Elaborate.
optimization_guide::proto::ComposeRequest request;
request.mutable_rewrite_params()->set_previous_response("Cucumbers");
request.mutable_rewrite_params()->set_length(
optimization_guide::proto::ComposeLength::COMPOSE_LONGER);
auto rewrite_matcher = EqualsProto(request);
EXPECT_CALL(session(), ExecuteModel(rewrite_matcher, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Tomatoes")));
})));
// Rewrite with Shorten.
request.mutable_rewrite_params()->set_previous_response("Tomatoes");
request.mutable_rewrite_params()->set_length(
optimization_guide::proto::ComposeLength::COMPOSE_SHORTER);
auto rewrite_shorten_matcher = EqualsProto(request);
EXPECT_CALL(session(), ExecuteModel(rewrite_shorten_matcher, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(OptimizationGuideStreamingResult(
ComposeResponse(true, "Potatoes")));
})));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillRepeatedly(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
page_handler()->Compose(user_input, compose::mojom::InputMode::kPolish,
false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
EXPECT_EQ("Cucumbers", result->result);
page_handler()->Rewrite(compose::mojom::StyleModifier::kLonger);
result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
EXPECT_EQ("Tomatoes", result->result);
histograms().ExpectBucketCount(
compose::kComposeRequestReason,
compose::ComposeRequestReason::kLengthElaborateRequest, 1);
histograms().ExpectBucketCount(
"Compose.Server.Request.Reason",
compose::ComposeRequestReason::kLengthElaborateRequest, 1);
page_handler()->Rewrite(compose::mojom::StyleModifier::kShorter);
result = test_future.Take();
histograms().ExpectBucketCount(
compose::kComposeRequestReason,
compose::ComposeRequestReason::kLengthShortenRequest, 1);
histograms().ExpectBucketCount(
"Compose.Server.Request.Reason",
compose::ComposeRequestReason::kLengthShortenRequest, 1);
// Make sure the async call to CloseUI completes before navigating away.
FlushMojo();
// Navigate page away to upload UKM metrics to the collector.
NavigateAndCommitActiveTab(GURL("about:blank"));
// Check Compose Session Event Counts.
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMainDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kFormalClicked, 0);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kCasualClicked, 0);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kElaborateClicked, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kShortenClicked, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kAnyModifierUsed, 1);
// Check session level UKM metrics.
auto session_ukm_entries = ukm_recorder().GetEntries(
ukm::builders::Compose_SessionProgress::kEntryName,
{ukm::builders::Compose_SessionProgress::kLengthenCountName,
ukm::builders::Compose_SessionProgress::kShortenCountName});
EXPECT_EQ(session_ukm_entries.size(), 1UL);
EXPECT_THAT(
session_ukm_entries[0].metrics,
testing::UnorderedElementsAre(
testing::Pair(
ukm::builders::Compose_SessionProgress::kLengthenCountName, 1),
testing::Pair(
ukm::builders::Compose_SessionProgress::kShortenCountName, 1)));
}
TEST_F(ChromeComposeClientTest, TestOfflineError) {
ShowDialogAndBindMojo();
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
std::move(callback).Run(
OptimizationGuideModelStreamingExecutionResult(
base::unexpected(
OptimizationGuideModelExecutionError::
FromModelExecutionError(
optimization_guide::
OptimizationGuideModelExecutionError::
ModelExecutionError::kGenericFailure)),
/*provided_by_on_device=*/false,
std::make_unique<ModelExecutionInfo>()));
})));
base::test::TestFuture<compose::mojom::ComposeResponsePtr> test_future;
EXPECT_CALL(compose_dialog(), ResponseReceived(_))
.WillOnce(
testing::Invoke([&](compose::mojom::ComposeResponsePtr response) {
test_future.SetValue(std::move(response));
}));
// Go offline and then run Compose.
network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
network::mojom::ConnectionType::CONNECTION_NONE);
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOffline, result->status);
}
TEST_F(ChromeComposeClientTest, TestInnerText) {
EXPECT_CALL(model_inner_text(), GetInnerText(_, _, _))
.WillOnce(testing::WithArg<2>(
testing::Invoke([&](content_extraction::InnerTextCallback callback) {
std::unique_ptr<content_extraction::InnerTextResult>
expected_inner_text =
std::make_unique<content_extraction::InnerTextResult>(
"inner_text", 123);
std::move(callback).Run(std::move(expected_inner_text));
})));
base::test::TestFuture<optimization_guide::proto::ComposeRequest> test_future;
EXPECT_CALL(session(), AddContext(_))
.WillOnce(testing::WithArg<0>(testing::Invoke(
[&](const google::protobuf::MessageLite& request_metadata) {
optimization_guide::proto::ComposeRequest request;
request.CheckTypeAndMergeFrom(request_metadata);
test_future.SetValue(request);
})));
ShowDialogAndBindMojo();
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
optimization_guide::proto::ComposeRequest result = test_future.Take();
std::string result_string;
EXPECT_TRUE(result.SerializeToString(&result_string));
EXPECT_EQ("inner_text", result.page_metadata().page_inner_text());
EXPECT_EQ(123u, result.page_metadata().page_inner_text_offset());
}
TEST_F(ChromeComposeClientTest, TestInnerTextNodeOffsetNotFound) {
EXPECT_CALL(model_inner_text(), GetInnerText(_, _, _))
.WillOnce(testing::WithArg<2>(
testing::Invoke([&](content_extraction::InnerTextCallback callback) {
std::unique_ptr<content_extraction::InnerTextResult>
expected_inner_text =
std::make_unique<content_extraction::InnerTextResult>(
"inner_text", std::nullopt);
std::move(callback).Run(std::move(expected_inner_text));
})));
base::test::TestFuture<optimization_guide::proto::ComposeRequest> test_future;
EXPECT_CALL(session(), AddContext(_))
.WillOnce(testing::WithArg<0>(testing::Invoke(
[&](const google::protobuf::MessageLite& request_metadata) {
optimization_guide::proto::ComposeRequest request;
request.CheckTypeAndMergeFrom(request_metadata);
test_future.SetValue(request);
})));
ShowDialogAndBindMojo();
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
optimization_guide::proto::ComposeRequest result = test_future.Take();
std::string result_string;
EXPECT_TRUE(result.SerializeToString(&result_string));
EXPECT_EQ("inner_text", result.page_metadata().page_inner_text());
histograms().ExpectUniqueSample(
compose::kInnerTextNodeOffsetFound,
compose::ComposeInnerTextNodeOffset::kNoOffsetFound, 1);
}
TEST_F(ChromeComposeClientTest, TestCloseReasonCanceledWhileWaiting) {
ShowDialogAndBindMojo();
EXPECT_CALL(session(), ExecuteModel(_, _))
.WillOnce(testing::WithArg<1>(testing::Invoke(
[&](optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback
callback) {
// This is a no-op.
})));
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
base::test::TestFuture<compose::mojom::OpenMetadataPtr> open_test_future;
page_handler()->RequestInitialState(open_test_future.GetCallback());
compose::mojom::OpenMetadataPtr result = open_test_future.Take();
EXPECT_TRUE(result->compose_state->has_pending_request);
client().CloseUI(compose::mojom::CloseReason::kCloseButton);
histograms().ExpectUniqueSample(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kCanceledBeforeResponseReceived, 1);
}
TEST_F(ChromeComposeClientTest, LaunchHatsSurveyDisabled) {
// Add something for the test to wait on after the close event is finished
// so we dont tear down too early.
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
ShowDialogAndBindMojo();
base::test::ScopedFeatureList features;
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
ASSERT_TRUE(compose_future.Take());
EXPECT_CALL(*mock_hats_service(),
LaunchSurveyForWebContents(kHatsSurveyTriggerComposeAcceptance, _,
_, _, _, _, _, _))
.Times(0);
client_page_handler()->CloseUI(compose::mojom::CloseReason::kInsertButton);
EXPECT_TRUE(log_uploaded_signal.Wait());
}
TEST_F(ChromeComposeClientTest, LaunchHatsSurveyEnabled) {
{
scoped_feature_list_.Reset();
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/
{compose::features::kHappinessTrackingSurveysForComposeAcceptance},
/*disabled_features=*/{});
compose::ResetConfigForTesting();
// Add something for the test to wait on after the close event is finished
// so we dont tear down too early.
base::test::TestFuture<void> log_uploaded_signal;
logs_uploader().WaitForLogUpload(log_uploaded_signal.GetCallback());
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
page_handler()->Compose("a user typed this",
compose::mojom::InputMode::kPolish, false);
ASSERT_TRUE(compose_future.Take());
const SurveyBitsData product_specific_bits_data = {
{compose::hats::HatsFields::kResponseModified, false},
{compose::hats::HatsFields::kSessionContainedFilteredResponse, false},
{compose::hats::HatsFields::kSessionContainedError, false},
{compose::hats::HatsFields::kSessionBeganWithNudge, false}};
EXPECT_CALL(
*mock_hats_service(),
LaunchSurveyForWebContents(kHatsSurveyTriggerComposeAcceptance, _,
product_specific_bits_data, _, _, _, _, _))
.Times(1);
client_page_handler()->CloseUI(compose::mojom::CloseReason::kInsertButton);
EXPECT_TRUE(log_uploaded_signal.Wait());
}
}
#if defined(GTEST_HAS_DEATH_TEST)
// Tests that the Compose client crashes the browser if a webcontents
// tries to bind mojo without opening the dialog at a non Compose URL.
TEST_F(ChromeComposeClientTest, NoStateCrashesAtOtherUrls) {
GTEST_FLAG_SET(death_test_style, "threadsafe");
// We skip showing the dialog here to validate that non special URLs check.
EXPECT_DEATH(BindMojo(), "");
}
// Tests that the Compose client crashes the browser if a webcontents
// sends any message when the dialog has not been shown.
TEST_F(ChromeComposeClientTest, TestCannotSendMessagesToNotShownDialog) {
GTEST_FLAG_SET(death_test_style, "threadsafe");
EXPECT_DEATH(page_handler()->SaveWebUIState(""), "");
}
// Tests that the Compose client crashes the browser if a webcontents
// tries to close the dialog when the dialog has not been shown.
TEST_F(ChromeComposeClientTest, TestCannotCloseNotShownDialog) {
GTEST_FLAG_SET(death_test_style, "threadsafe");
EXPECT_DEATH(
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton),
"");
}
// Tests that the Compose client crashes the browser if a webcontents
// tries to close the dialog when the dialog has not been shown.
TEST_F(ChromeComposeClientTest, TestCannotSendMessagesAfterClosingDialog) {
GTEST_FLAG_SET(death_test_style, "threadsafe");
ShowDialogAndBindMojo();
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
// Any message after closing the session will crash.
EXPECT_DEATH(page_handler()->SaveWebUIState(""), "");
}
// Tests that the Compose client crashes the browser if a webcontents
// sends any more messages after closing the dialog at
// chrome-untrusted://compose.
TEST_F(ChromeComposeClientTest,
TestCannotSendMessagesAfterClosingDialogAtChromeCompose) {
GTEST_FLAG_SET(death_test_style, "threadsafe");
NavigateAndCommitActiveTab(GURL(chrome::kChromeUIUntrustedComposeUrl));
// We skip the dialog showing here, as there is no dialog required at this
// URL.
BindMojo();
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
// Any message after closing the session will crash.
EXPECT_DEATH(page_handler()->SaveWebUIState(""), "");
}
#endif // GTEST_HAS_DEATH_TEST
class ComposePopupAutofillDriverTest : public ChromeComposeClientTest {
public:
void SetUp() override {
ChromeComposeClientTest::SetUp();
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = true;
config.proactive_nudge_show_probability = 1.0;
config.proactive_nudge_field_per_navigation = true;
config.proactive_nudge_segmentation = false;
config.proactive_nudge_focus_delay = base::Microseconds(8);
config.proactive_nudge_text_settled_delay = base::Microseconds(16);
config.proactive_nudge_text_change_count = 3;
config.selection_nudge_delay = base::Microseconds(4);
config.selection_nudge_enabled = true;
config.selection_nudge_length = 5;
}
// Creates a mock form with |num_fields|.
autofill::FormData CreateTestFormData(int num_fields = 1) {
autofill::FormData form;
form.set_url(web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
std::vector<autofill::FormFieldData> fields;
for (int i = 0; i < num_fields; ++i) {
fields.push_back(autofill::test::CreateTestFormField(
"label", "name", "value", autofill::FormControlType::kTextArea));
}
form.set_fields(fields);
for (int i = 0; i < num_fields; ++i) {
autofill::FormFieldData& field = test_api(form).field(i);
field.set_origin(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
field.set_host_frame(form.host_frame());
}
return form;
}
autofill::ContentAutofillDriver* CreateAutofillDriver(
autofill::FormData form_data) {
autofill::ContentAutofillDriver* autofill_driver =
autofill::ContentAutofillDriver::GetForRenderFrameHost(
web_contents()->GetPrimaryMainFrame());
EXPECT_TRUE(autofill_driver);
{
autofill::TestAutofillManagerWaiter waiter(
autofill_driver->GetAutofillManager(),
{autofill::AutofillManagerEvent::kFormsSeen});
autofill_driver->renderer_events().FormsSeen(
/*updated_forms=*/{form_data},
/*removed_forms=*/{});
EXPECT_TRUE(waiter.Wait(/*num_awaiting_calls=*/1));
}
return autofill_driver;
}
};
TEST_F(ComposePopupAutofillDriverTest, TestSelectionNudgeNoProactiveNudge) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = false;
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker with
// only the selection nudge enabled.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
// No timer should be running since the proactive nudge is disabled.
ASSERT_FALSE(client().IsPopupTimerRunning());
// Wait for when the timer would finish and ShouldTriggerPopup still fails.
task_environment()->FastForwardBy(base::Microseconds(9));
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Begin showing the selection nudge.
// Signal that the caret moved in the field with a valid selection.
field_data.set_selected_text(u"12345");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(), /*caret_bounds=*/gfx::Rect());
// The timer should now be running.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Move forward until timer should expire.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will now succeed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
}
TEST_F(ComposePopupAutofillDriverTest, TestSelectionNudgeEnabled) {
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(7));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
// The trigger will now succeed.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Begin showing the selection nudge.
// Signal that the caret moved in the field with a valid selection.
field_data.set_selected_text(u"12345");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(), /*caret_bounds=*/gfx::Rect());
// The timer should now be running.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Extending the selection extends the timer.
field_data.set_selected_text(u"123456");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
// The timer should still be running.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Move forward until timer should expire.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will now succeed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
histograms().ExpectUniqueSample(
compose::kComposeSelectionNudgeCtr,
compose::ComposeNudgeCtrEvent::kNudgeDisplayed, 1);
histograms().ExpectUniqueSample(
compose::kComposeProactiveNudgeCtr,
compose::ComposeNudgeCtrEvent::kNudgeDisplayed, 1);
}
TEST_F(ComposePopupAutofillDriverTest, TestSelectionTooShort) {
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(7));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
// The trigger will now succeed.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
// A selection that is to short will not trigger the nudge
field_data.set_selected_text(u"1234");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will fail since the selection was not long enough.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// But it can be shown again if the selection is long enough.
field_data.set_selected_text(u"some text was selected");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
// The timer should now be running.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// A selection that is now too short will cancel the nudge timer.
field_data.set_selected_text(u"one");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
// The timer should be canceled.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
// The selection nudge will not trigger.
task_environment()->FastForwardBy(base::Microseconds(5));
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Confirm that after a the selection timer is canceled it can be started
// again with a valid selection.
field_data.set_selected_text(u"some text was selected");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
// The timer should now be running.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Move forward until timer should expire.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will now succeed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
}
TEST_F(ComposePopupAutofillDriverTest, TestSelectionNudgeLostFocus) {
autofill::FormData form_data = CreateTestFormData(2);
autofill::FormFieldData& field_data0 = test_api(form_data).field(0);
autofill::FormFieldData& field_data1 = test_api(form_data).field(1);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(7));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
// The trigger will now succeed.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Test that losing focus and returning to a field will still show the
// selection nudge.
// Trigger the popup on field 1 losing focus on field 0.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data1,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(7));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data1,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Now trigger on field 0 again. This will fail and not start a timer since
// proactive_nudge_field_per_navigation = true.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Wait for when the timer would finish and ShouldTriggerPopup still fails.
task_environment()->FastForwardBy(base::Microseconds(9));
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Trigger a valid selection and confirm that the selection nudge can still be
// shown.
field_data0.set_selected_text(u"some text was selected");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data0.global_id(),
/*caret_bounds=*/gfx::Rect());
// The timer should now be running.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Move forward until timer should expire.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will now succeed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
}
TEST_F(ComposePopupAutofillDriverTest,
TestSelectionNudgeBlockedBySegmentation) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_segmentation = true;
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
EXPECT_CALL(GetSegmentationPlatformService(),
GetClassificationResult(_, _, _, _))
.WillOnce(testing::WithArg<3>(testing::Invoke(
[](segmentation_platform::ClassificationResultCallback callback) {
auto result = segmentation_platform::ClassificationResult(
segmentation_platform::PredictionStatus::kSucceeded);
result.request_id = kTrainingRequestId;
result.ordered_labels = {
segmentation_platform::kComposePrmotionLabelDontShow};
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), result));
})));
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(7));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
// The trigger will now be blocked by segmentation.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Signal that the caret moved in the field with a valid selection.
field_data.set_selected_text(u"12345");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
// The timer should not be running since the segmentation blocked the nudge.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Move forward until the timer would be complete if it were running.
task_environment()->FastForwardBy(base::Microseconds(4));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will fail since the selection nudge was blocked.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
}
TEST_F(ComposePopupAutofillDriverTest, TestCaretMovementExtendsNudgeDelay) {
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(7));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Signal that the caret moved in the field with no selection.
field_data.set_selected_text(u"");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
// Moving the caret should extend the timer so it is still running.
task_environment()->FastForwardBy(base::Microseconds(7));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Move forward until timer should expire.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will now succeed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
}
TEST_F(ComposePopupAutofillDriverTest, TestSelectionNudgeNoDelay) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.selection_nudge_delay = base::Microseconds(0);
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(7));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
// The trigger will now succeed.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Signal that the caret moved in the field with a valid selection.
field_data.set_selected_text(u"12345");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
// The timer should not be running since there is no delay.
ASSERT_FALSE(client().IsPopupTimerRunning());
task_environment()->FastForwardBy(base::Microseconds(1));
// Should trigger will not succeed since a delay of zero disabled the nudge.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
}
TEST_F(ComposePopupAutofillDriverTest, TestSelectionNudgeDisabled) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.selection_nudge_enabled = false;
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(7));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
// The trigger will now succeed.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Signal that the caret moved in the field with a valid selection.
field_data.set_selected_text(u"12345");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
// The timer should not be running since the selection nudge is disabled.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will fail since the selection nudge is disabled.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Move forward until the timer would be complete if it were running.
task_environment()->FastForwardBy(base::Microseconds(4));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will fail since the selection nudge is disabled.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
}
TEST_F(ComposePopupAutofillDriverTest, TestSelectionNudgeOncePerFocus) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_enabled = false;
config.selection_nudge_once_per_focus = true;
autofill::FormData form_data = CreateTestFormData(2);
autofill::FormFieldData& field_data0 = test_api(form_data).field(0);
autofill::FormFieldData& field_data1 = test_api(form_data).field(1);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker for field 0
// with only the selection nudge enabled.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
// No timer should be running since the proactive nudge is disabled.
ASSERT_FALSE(client().IsPopupTimerRunning());
// Trigger the selection nudge on field 0
field_data0.set_selected_text(u"some text was selected");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data0.global_id(),
/*caret_bounds=*/gfx::Rect());
// The timer should now be running.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Move forward until timer should expire.
task_environment()->FastForwardBy(base::Microseconds(2));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will now succeed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Trigger the selection nudge on field 0 for a second time.
field_data0.set_selected_text(u"some text was selected");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data0.global_id(),
/*caret_bounds=*/gfx::Rect());
// Timer should not be running since the selection nudge was already shown.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Move forward until timer should expire.
task_environment()->FastForwardBy(base::Microseconds(2));
// Make sure should trigger does not succeed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Test that losing focus and returning to a field will still show the
// selection nudge.
// Trigger the popup on field 1 losing focus on field 0.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data1,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data1,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Now trigger on field 0 again. This will fail and not start a timer since
// the proactive nudge is not enabled.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Wait for when the timer would finish and ShouldTriggerPopup still fails.
task_environment()->FastForwardBy(base::Microseconds(4));
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Trigger a selection and confirm that the selection nudge can be shown.
field_data0.set_selected_text(u"some text was selected");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data0.global_id(),
/*caret_bounds=*/gfx::Rect());
// The timer should now be running.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Move forward until timer should expire.
task_environment()->FastForwardBy(base::Microseconds(2));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will now succeed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data0,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
}
TEST_F(ComposePopupAutofillDriverTest,
TestFocusNudgeExtendedToTextChangeNudge) {
compose::Config& config = compose::GetMutableConfigForTesting();
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Simulate engough text change events to trigger the text change nudge.
// A text change consists of both |AfterTextFieldValueChanged| and
// |AfterCaretMovedInFormField| (since typing also moves the caret).
for (int i = 0; i < config.proactive_nudge_text_change_count; ++i) {
field_data.set_value(u"new text value");
autofill_driver->GetAutofillManager().OnTextFieldValueChanged(
form_data, field_data.global_id(), /*timestamp=*/{});
field_data.set_selected_text(u"");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
task_environment()->FastForwardBy(base::Microseconds(1));
}
task_environment()->FastForwardBy(config.proactive_nudge_text_settled_delay -
base::Microseconds(2));
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
task_environment()->FastForwardBy(base::Microseconds(1));
// Should trigger will succeed since the text input delay has passed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
}
TEST_F(ComposePopupAutofillDriverTest, TestFocusNudgeExtendedToSelectionNudge) {
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Begin showing the selection nudge.
// Signal that the caret moved in the field with a valid selection.
field_data.set_selected_text(u"12345");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(), /*caret_bounds=*/gfx::Rect());
// The timer should now be running.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Move forward until timer should expire.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will now succeed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
}
TEST_F(ComposePopupAutofillDriverTest, TestFocusNudgeCanceledBySelectionNudge) {
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Begin showing the selection nudge.
// Signal that the caret moved in the field with a valid selection.
field_data.set_selected_text(u"12345");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(), /*caret_bounds=*/gfx::Rect());
// The timer should now be running.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// A selection that is now too short will cancel the nudge timer.
field_data.set_selected_text(u"one");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
// The timer should be canceled.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
// The selection nudge will not trigger.
task_environment()->FastForwardBy(base::Microseconds(5));
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
}
TEST_F(ComposePopupAutofillDriverTest,
TestFocusNudgeDisabledTextChangeNudgeEnabled) {
compose::Config& config = compose::GetMutableConfigForTesting();
config.proactive_nudge_focus_delay = base::Seconds(0);
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup does not start the timmer since the
// focus nudge is disabled.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_FALSE(client().IsPopupTimerRunning());
task_environment()->FastForwardBy(base::Microseconds(2));
// Should trigger will fail since the focus nudge did not start.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Simulate engough text change events to trigger the text change nudge.
// A text change consists of both |AfterTextFieldValueChanged| and
// |AfterCaretMovedInFormField| (since typing also moves the caret).
for (int i = 0; i < config.proactive_nudge_text_change_count; ++i) {
field_data.set_value(u"new text value");
autofill_driver->GetAutofillManager().OnTextFieldValueChanged(
form_data, field_data.global_id(), /*timestamp=*/{});
field_data.set_selected_text(u"");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(),
/*caret_bounds=*/gfx::Rect());
task_environment()->FastForwardBy(base::Microseconds(1));
}
task_environment()->FastForwardBy(config.proactive_nudge_text_settled_delay -
base::Microseconds(2));
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
task_environment()->FastForwardBy(base::Microseconds(1));
// Should trigger will succeed since the text input delay has passed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
}
TEST_F(ComposePopupAutofillDriverTest, TestCloseSessionResetsNudgeTracker) {
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(8));
// The trigger will succeed since enough time passed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Simiulate closing the session with the "X" button.
compose::ComposeSessionEvents events{};
client().OnSessionComplete(
field_data.global_id(),
compose::ComposeSessionCloseReason::kCloseButtonPressed, events);
// Signal that the caret moved in the field with a valid selection.
field_data.set_selected_text(u"12345");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(), /*caret_bounds=*/gfx::Rect());
// The timer should not be running since closing the session resets the nudge
// tracker.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Move forward until timer should expire.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will not succeed since the timer never started.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
}
TEST_F(ComposePopupAutofillDriverTest, TestSelectionNudgeEntryPointMetrics) {
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
autofill::ContentAutofillDriver* autofill_driver =
CreateAutofillDriver(form_data);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(7));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Signal that the caret moved in the field with a valid selection.
field_data.set_selected_text(u"12345");
autofill_driver->GetAutofillManager().OnCaretMovedInFormField(
form_data, field_data.global_id(), /*caret_bounds=*/gfx::Rect());
// The timer should now be running.
task_environment()->FastForwardBy(base::Microseconds(3));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Move forward until timer should expire.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Should trigger will now succeed.
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
// Simulate clicking on the nudge to open compose.
ShowDialogAndBindMojoWithFieldData(
field_data, base::NullCallback(),
autofill::AutofillComposeDelegate::UiEntryPoint::kAutofillPopup);
// Close session to record UMA
client().CloseUI(compose::mojom::CloseReason::kInsertButton);
// Check that the session entry point histogram is recorded.
histograms().ExpectUniqueSample(compose::kComposeStartSessionEntryPoint,
compose::ComposeEntryPoint::kSelectionNudge,
1);
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.StartedSession.SelectionNudge"));
}
TEST_F(ComposePopupAutofillDriverTest, TestProactiveNudgeEntryPointMetrics) {
autofill::FormData form_data = CreateTestFormData();
autofill::FormFieldData& field_data = test_api(form_data).field(0);
// The first call to ShouldTriggerPopup starts the nudge tracker timers.
ASSERT_FALSE(client().ShouldTriggerPopup(
form_data, field_data,
autofill::AutofillSuggestionTriggerSource::kTextFieldValueChanged));
task_environment()->FastForwardBy(base::Microseconds(7));
ASSERT_TRUE(client().IsPopupTimerRunning());
// Should trigger will fail since not enough time has passed.
ASSERT_FALSE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_TRUE(client().IsPopupTimerRunning());
// The trigger will now succeed.
task_environment()->FastForwardBy(base::Microseconds(1));
ASSERT_TRUE(
client().ShouldTriggerPopup(form_data, field_data,
autofill::AutofillSuggestionTriggerSource::
kComposeDelayedProactiveNudge));
ASSERT_FALSE(client().IsPopupTimerRunning());
// Simulate clicking on the nudge to open compose.
ShowDialogAndBindMojoWithFieldData(
field_data, base::NullCallback(),
autofill::AutofillComposeDelegate::UiEntryPoint::kAutofillPopup);
// Close session to record UMA
client().CloseUI(compose::mojom::CloseReason::kInsertButton);
// Check that the session entry point histogram is recorded.
histograms().ExpectUniqueSample(compose::kComposeStartSessionEntryPoint,
compose::ComposeEntryPoint::kProactiveNudge,
1);
EXPECT_EQ(1, user_action_tester().GetActionCount(
"Compose.StartedSession.ProactiveNudge"));
}