blob: 4e735e1ddfb1fa0e957541369163f466aa283435 [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/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 "chrome/browser/compose/compose_enabling.h"
#include "chrome/common/compose/compose.mojom.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/core/common/unique_ids.h"
#include "components/compose/core/browser/compose_features.h"
#include "components/compose/core/browser/compose_metrics.h"
#include "components/compose/core/browser/config.h"
#include "components/optimization_guide/core/model_quality/feature_type_map.h"
#include "components/optimization_guide/core/model_quality/model_quality_log_entry.h"
#include "components/optimization_guide/core/optimization_guide_features.h"
#include "components/optimization_guide/core/optimization_guide_model_executor.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_service.pb.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"
using ::base::test::EqualsProto;
using base::test::RunOnceCallback;
using testing::_;
using ComposeCallback = base::OnceCallback<void(const std::u16string&)>;
using optimization_guide::OptimizationGuideModelExecutionError;
using optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback;
using optimization_guide::OptimizationGuideModelStreamingExecutionResult;
using optimization_guide::StreamingResponse;
namespace {
const uint64_t kSessionIdHigh = 1234;
const uint64_t kSessionIdLow = 5678;
constexpr char kTypeURL[] =
"type.googleapis.com/optimization_guide.proto.ComposeResponse";
class MockInnerText : public InnerTextProvider {
public:
MOCK_METHOD(void,
GetInnerText,
(content::RenderFrameHost & host,
absl::optional<int> node_id,
content_extraction::InnerTextCallback callback));
};
class MockModelExecutor
: public optimization_guide::OptimizationGuideModelExecutor {
public:
MOCK_METHOD(std::unique_ptr<Session>,
StartSession,
(optimization_guide::proto::ModelExecutionFeature feature));
MOCK_METHOD(void,
ExecuteModel,
(optimization_guide::proto::ModelExecutionFeature feature,
const google::protobuf::MessageLite& request_metadata,
optimization_guide::OptimizationGuideModelExecutionResultCallback
callback));
};
class MockModelQualityLogsUploader
: public optimization_guide::ModelQualityLogsUploader {
public:
MOCK_METHOD(
void,
UploadModelQualityLogs,
(std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry));
};
class MockSession
: public optimization_guide::OptimizationGuideModelExecutor::Session {
public:
MOCK_METHOD(void,
AddContext,
(const google::protobuf::MessageLite& request_metadata));
MOCK_METHOD(
void,
ExecuteModel,
(const google::protobuf::MessageLite& request_metadata,
optimization_guide::
OptimizationGuideModelExecutionResultStreamingCallback callback));
};
// A wrapper that passes through calls to the underlying MockSession. Allows for
// easily mocking calls with a single session object.
class MockSessionWrapper
: public optimization_guide::OptimizationGuideModelExecutor::Session {
public:
explicit MockSessionWrapper(MockSession& session) : session_(session) {}
void AddContext(
const google::protobuf::MessageLite& request_metadata) override {
session_->AddContext(request_metadata);
}
void ExecuteModel(
const google::protobuf::MessageLite& request_metadata,
optimization_guide::OptimizationGuideModelExecutionResultStreamingCallback
callback) override {
session_->ExecuteModel(request_metadata, std::move(callback));
}
private:
raw_ref<MockSession> session_;
};
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:
void SetUp() override {
scoped_compose_enabled_ = ComposeEnabling::ScopedEnableComposeForTesting();
BrowserWithTestWindowTest::SetUp();
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>();
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
true);
SetPrefsForComposeMSBBState(true);
AddTab(browser(), GetPageUrl());
client_ = ChromeComposeClient::FromWebContents(web_contents());
client_->SetModelExecutorForTest(&model_executor_);
client_->SetInnerTextProviderForTest(&model_inner_text_);
client_->SetSkipShowDialogForTest(true);
client_->SetModelQualityLogsUploaderForTest(&model_quality_logs_uploader_);
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<MockSessionWrapper>(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::ModelQualityLogEntry>(
std::make_unique<optimization_guide::proto::
LogAiDataRequest>()))));
})));
test_timer_ = std::make_unique<base::ScopedMockElapsedTimersForTest>();
}
void TearDown() override {
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_; }
MockSession& session() { return session_; }
MockModelQualityLogsUploader& model_quality_logs_uploader() {
return model_quality_logs_uploader_;
}
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().selected_text = selection;
}
// Emulate selected text truncation performed by Autofill.
void SetSelectionWithTruncation(const std::u16string& selection,
size_t max_length) {
field_data().selected_text = selection.substr(0, max_length);
}
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::ComposeRequest request;
request.mutable_generate_params()->set_user_input(user_input);
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) {
optimization_guide::proto::Any any;
any.set_type_url(kTypeURL);
compose_response.SerializeToString(any.mutable_value());
return StreamingResponse{
.response = any,
.is_complete = is_complete,
};
}
OptimizationGuideModelStreamingExecutionResult
OptimizationGuideStreamingResult(
const optimization_guide::proto::ComposeResponse compose_response,
bool is_complete = true,
bool provided_by_on_device = false,
std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry =
nullptr) {
return OptimizationGuideModelStreamingExecutionResult(
base::ok(OptimizationGuideResponse(compose_response, is_complete)),
provided_by_on_device, std::move(log_entry));
}
const base::HistogramTester& histograms() const { return histogram_tester_; }
const base::UserActionTester& user_action_tester() const {
return user_action_tester_;
}
// 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));
}));
}
base::test::ScopedFeatureList scoped_feature_list_;
private:
raw_ptr<ChromeComposeClient> client_;
testing::NiceMock<MockModelQualityLogsUploader> model_quality_logs_uploader_;
testing::NiceMock<MockModelExecutor> model_executor_;
testing::NiceMock<MockInnerText> model_inner_text_;
testing::NiceMock<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_;
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_;
std::unique_ptr<base::ScopedMockElapsedTimersForTest> test_timer_;
ComposeEnabling::ScopedOverride scoped_compose_enabled_;
};
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("", 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 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::kFirstRequest,
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 a 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::kDialogShown, 1);
histograms().ExpectBucketCount(
"Compose.Server.Session.EventCounts",
compose::ComposeSessionEventTypes::kDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kCreateClicked, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kInsertClicked, 1);
histograms().ExpectBucketCount("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("", 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(nullptr);
// Simulate insert call from Compose dialog.
page_handler()->AcceptComposeResult(base::NullCallback());
client_page_handler()->CloseUI(compose::mojom::CloseReason::kInsertButton);
FlushMojo();
histograms().ExpectBucketCount("Compose.Session.EvalLocation",
compose::SessionEvalLocation::kMixed, 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().ExpectBucketCount("Compose.Session.EvalLocation",
compose::SessionEvalLocation::kNone, 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});
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)));
}
TEST_F(ChromeComposeClientTest, TestComposeWithIncompleteResponsesAnimated) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
{optimization_guide::features::kOptimizationGuideOnDeviceModel,
compose::features::kComposeTextOutputAnimation},
{});
base::HistogramTester histogram_tester;
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)), _))
.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, 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.
histogram_tester.ExpectUniqueSample(compose::kComposeRequestStatus,
compose::mojom::ComposeStatus::kOk, 1);
// Check that a single request duration OK metric was emitted.
histogram_tester.ExpectTotalCount(
base::StrCat({"Compose", compose::kComposeRequestDurationOkSuffix}), 1);
// Check that no request duration Error metric was emitted.
histogram_tester.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}, {});
base::HistogramTester histogram_tester;
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)), _))
.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, 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},
{});
base::HistogramTester histogram_tester;
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)), _))
.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)), _))
.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, false);
EXPECT_EQ("Cucu", partial_response.Get()->result);
page_handler()->Compose(input2, 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.
histogram_tester.ExpectUniqueSample(compose::kComposeRequestStatus,
compose::mojom::ComposeStatus::kOk, 1);
// Check that a single request duration OK metric was emitted.
histogram_tester.ExpectTotalCount(
base::StrCat({"Compose", compose::kComposeRequestDurationOkSuffix}), 1);
// Check that no request duration Error metric was emitted.
histogram_tester.ExpectTotalCount(
base::StrCat({"Compose", compose::kComposeRequestDurationErrorSuffix}),
0);
}
TEST_F(ChromeComposeClientTest, TestComposeParams) {
ShowDialogAndBindMojo();
std::string user_input = "a user typed this";
auto matcher = EqualsProto(ComposeRequest(user_input));
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, 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<optimization_guide::ModelQualityLogEntry>(
std::make_unique<
optimization_guide::proto::LogAiDataRequest>())));
})));
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));
}));
base::test::TestFuture<
std::unique_ptr<optimization_guide::ModelQualityLogEntry>>
quality_test_future;
EXPECT_CALL(model_quality_logs_uploader(), UploadModelQualityLogs(_))
.WillRepeatedly(testing::Invoke(
[&](std::unique_ptr<optimization_guide::ModelQualityLogEntry>
response) {
quality_test_future.SetValue(std::move(response));
}));
page_handler()->Compose("a user typed this", false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kServerError, result->status);
// Check that the quality modeling log is still correct
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
std::unique_ptr<optimization_guide::ModelQualityLogEntry> quality_result =
quality_test_future.Take();
EXPECT_EQ(
kSessionIdHigh,
quality_result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->session_id()
.high());
EXPECT_EQ(
kSessionIdLow,
quality_result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->session_id()
.low());
}
// 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", 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 a 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", 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", 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", 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 saved WebUI state is 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 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("", 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 the dialog showing 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", 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 closing after showing the dialog does not crash the browser.
TEST_F(ChromeComposeClientTest, TestCancelMetrics) {
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 the dialog showing 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().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().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);
}
// Tests that opening the dialog with selected text clears existing state.
TEST_F(ChromeComposeClientTest, TestClearStateWhenOpenWithSelectedText) {
ShowDialogAndBindMojo();
page_handler()->SaveWebUIState("web ui state");
field_data().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().ExpectBucketCount(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kNewSessionWithSelectedText, 1);
}
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.
TEST_F(ChromeComposeClientTest, TestUndoUnavailableFirstCompose) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
page_handler()->Compose("", 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("", 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("", 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::kDialogShown, 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.
TEST_F(ChromeComposeClientTest, TestUndoStackMultipleUndos) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
page_handler()->SaveWebUIState("first state");
page_handler()->Compose("", 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("", 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("", 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("", 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("", 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("", 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 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", 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());
}
TEST_F(ChromeComposeClientTest, LearnMoreLinkOpensCorrectURL) {
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());
}
TEST_F(ChromeComposeClientTest, ResetClientOnNavigation) {
ShowDialogAndBindMojo();
page_handler()->SaveWebUIState("first state");
page_handler()->Compose("", false);
autofill::FormFieldData field_2;
field_2.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 session should be deleted.
EXPECT_EQ(0, client().GetSessionCountForTest());
}
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("", false);
compose::mojom::ComposeResponsePtr response = compose_future.Take();
page_handler()->Compose("", true);
response = compose_future.Take();
page_handler()->Compose("", 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().ExpectBucketCount(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kCloseButtonPressed, 1);
// Expect that three total Compose calls were recorded.
histograms().ExpectBucketCount("Compose.Session.ComposeCount.Ignored", 3, 1);
histograms().ExpectBucketCount("Compose.Server.Session.ComposeCount.Ignored",
3, 1);
// Expect that two of the Compose calls were from edits.
histograms().ExpectBucketCount("Compose.Session.SubmitEditCount.Ignored", 2,
1);
histograms().ExpectBucketCount(
"Compose.Server.Session.SubmitEditCount.Ignored", 2, 1);
// Expect that two undos were done.
histograms().ExpectBucketCount("Compose.Session.UndoCount.Ignored", 2, 1);
histograms().ExpectBucketCount("Compose.Server.Session.UndoCount.Ignored", 2,
1);
// Expect that the dialog was shown twice.
histograms().ExpectBucketCount("Compose.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().ExpectBucketCount(compose::kComposeSessionOverOneDay, 0, 1);
// No FRE related close reasons should have been recorded.
histograms().ExpectTotalCount(compose::kComposeFirstRunSessionCloseReason, 0);
}
TEST_F(ChromeComposeClientTest, CloseButtonMSBBHistogramTest) {
SetPrefsForComposeMSBBState(false);
ShowDialogAndBindMojo();
client().CloseUI(compose::mojom::CloseReason::kMSBBCloseButton);
histograms().ExpectBucketCount(
compose::kComposeMSBBSessionCloseReason,
compose::ComposeMSBBSessionCloseReason::kMSBBCloseButtonPressed, 1);
histograms().ExpectBucketCount(
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().ExpectBucketCount(compose::kComposeSessionOverOneDay, 0, 1);
}
TEST_F(ChromeComposeClientTest,
CloseButtonMSBBEnabledDuringSessionHistogramTest) {
SetPrefsForComposeMSBBState(false);
ShowDialogAndBindMojo();
SetPrefsForComposeMSBBState(true);
// Show the dialog a second time.
ShowDialogAndBindMojo();
client().CloseUI(compose::mojom::CloseReason::kCloseButton);
histograms().ExpectBucketCount(
compose::kComposeSessionComposeCount + std::string(".Ignored"),
0, // Expect that zero total Compose calls were recorded.
1);
histograms().ExpectBucketCount(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kCloseButtonPressed, 1);
histograms().ExpectBucketCount(
compose::kComposeMSBBSessionCloseReason,
compose::ComposeMSBBSessionCloseReason::kMSBBAcceptedWithoutInsert, 1);
histograms().ExpectBucketCount(
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 Compose Session Event Counts
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kDialogShown, 1);
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kFREShown,
0);
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMSBBShown,
1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMSBBEnabled, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kInsertClicked, 0);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kCloseClicked, 1);
}
TEST_F(ChromeComposeClientTest, FirstRunCloseDialogHistogramTest) {
// Enable FRE and show the dialog.
GetProfile()->GetPrefs()->SetBoolean(prefs::kPrefHasCompletedComposeFRE,
false);
ShowDialogAndBindMojo();
client().CloseUI(compose::mojom::CloseReason::kFirstRunCloseButton);
histograms().ExpectBucketCount(
compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFirstRunSessionCloseReason::kCloseButtonPressed, 1);
// Expect that the dialog was shown once ending without FRE completed.
histograms().ExpectBucketCount(
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().ExpectBucketCount(compose::kComposeSessionOverOneDay, 0, 1);
// Show the FRE dialog and end the session by re-opening with selection
ShowDialogAndBindMojo();
field_data().value = u"user selected text";
SetSelection(u"selected text");
ShowDialogAndBindMojo();
histograms().ExpectBucketCount(
compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFirstRunSessionCloseReason::kNewSessionWithSelectedText,
1);
histograms().ExpectBucketCount(
compose::kComposeFirstRunSessionDialogShownCount +
std::string(".Ignored"),
1, // Expect that the dialog was shown once.
2);
// Throughout all sessions no main dialog metrics should have been logged, as
// the dialog never moved past the FRE.
histograms().ExpectTotalCount(compose::kComposeSessionCloseReason, 0);
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().ExpectBucketCount(
compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFirstRunSessionCloseReason::
kFirstRunDisclaimerAcknowledgedWithoutInsert,
1);
// Expect that the dialog was shown twice ending with FRE completed.
histograms().ExpectBucketCount(
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().ExpectBucketCount(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kCloseButtonPressed, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionDialogShownCount + std::string(".Ignored"),
1, // The dialog was only shown once after having proceeded past FRE.
1);
}
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().ExpectBucketCount(compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFirstRunSessionCloseReason::
kFirstRunDisclaimerAcknowledgedWithInsert,
1);
// Check Compose Session Event Counts
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kFREShown,
1);
histograms().ExpectBucketCount(compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kMSBBShown,
0);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kStartedWithSelection, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kInsertClicked, 1);
}
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));
}
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("", false);
compose::mojom::ComposeResponsePtr response = compose_future.Take();
page_handler()->Compose("", true);
response = compose_future.Take();
page_handler()->Compose("", 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().ExpectBucketCount(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kAcceptedSuggestion, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionComposeCount + std::string(".Accepted"),
3, // Expect that three Compose calls were recorded.
1);
histograms().ExpectBucketCount(
compose::kComposeSessionUpdateInputCount + std::string(".Accepted"),
2, // Expect that two of the Compose calls were from edits.
1);
histograms().ExpectBucketCount(
compose::kComposeSessionUndoCount + std::string(".Accepted"),
1, // Expect that one undo was done.
1);
histograms().ExpectBucketCount(
compose::kComposeSessionDialogShownCount + std::string(".Accepted"),
3, // Expect that the dialog was shown twice.
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().ExpectBucketCount(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().ExpectBucketCount(
compose::kComposeSessionCloseReason,
compose::ComposeSessionCloseReason::kEndedImplicitly, 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().ExpectBucketCount(
compose::kComposeFirstRunSessionCloseReason,
compose::ComposeFirstRunSessionCloseReason::kEndedImplicitly, 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();
// 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());
}
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().ExpectBucketCount(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);
}
TEST_F(ChromeComposeClientTest, TestComposeQualitySessionId) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(2);
base::test::TestFuture<
std::unique_ptr<optimization_guide::ModelQualityLogEntry>>
quality_test_future;
EXPECT_CALL(model_quality_logs_uploader(), UploadModelQualityLogs(_))
.WillRepeatedly(testing::Invoke(
[&](std::unique_ptr<optimization_guide::ModelQualityLogEntry>
response) {
quality_test_future.SetValue(std::move(response));
}));
page_handler()->Compose("a user typed this", false);
EXPECT_TRUE(compose_future.Wait());
// Reset future for second compose call.
compose_future.Clear();
page_handler()->Compose("a user typed that", false);
EXPECT_TRUE(compose_future.Wait());
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.";
// This take should clear the test future for the second commit.
std::unique_ptr<optimization_guide::ModelQualityLogEntry> result =
quality_test_future.Take();
EXPECT_EQ(kSessionIdHigh,
result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->session_id()
.high());
EXPECT_EQ(kSessionIdLow,
result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->session_id()
.low());
// Close UI to submit quality logs.
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
result = quality_test_future.Take();
EXPECT_EQ(kSessionIdHigh,
result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->session_id()
.high());
EXPECT_EQ(kSessionIdLow,
result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->session_id()
.low());
}
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<optimization_guide::ModelQualityLogEntry>(
std::make_unique<
optimization_guide::proto::LogAiDataRequest>())));
})));
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<
std::unique_ptr<optimization_guide::ModelQualityLogEntry>>
quality_test_future;
EXPECT_CALL(model_quality_logs_uploader(), UploadModelQualityLogs(_))
.WillRepeatedly(testing::Invoke(
[&](std::unique_ptr<optimization_guide::ModelQualityLogEntry>
response) {
quality_test_future.SetValue(std::move(response));
}));
page_handler()->Compose("a user typed this", 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", false);
compose_result = compose_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kServerError,
compose_result->status);
std::unique_ptr<optimization_guide::ModelQualityLogEntry> quality_result =
quality_test_future.Take();
// Ensure that a quality log is emitted after a second compose error.
EXPECT_EQ(
kSessionIdLow,
quality_result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->session_id()
.low());
// Close UI to submit remaining quality logs.
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
quality_result = quality_test_future.Take();
EXPECT_EQ(
base::ScopedMockElapsedTimersForTest::kMockElapsedTime.InMilliseconds(),
quality_result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->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);
}
TEST_F(ChromeComposeClientTest, TestComposeQualityLatency) {
ShowDialogAndBindMojo();
base::test::TestFuture<compose::mojom::ComposeResponsePtr> compose_future;
BindComposeFutureToOnResponseReceived(compose_future);
EXPECT_CALL(session(), ExecuteModel(_, _)).Times(2);
base::test::TestFuture<
std::unique_ptr<optimization_guide::ModelQualityLogEntry>>
quality_test_future;
EXPECT_CALL(model_quality_logs_uploader(), UploadModelQualityLogs(_))
.WillRepeatedly(testing::Invoke(
[&](std::unique_ptr<optimization_guide::ModelQualityLogEntry>
response) {
quality_test_future.SetValue(std::move(response));
}));
page_handler()->Compose("a user typed this", false);
EXPECT_TRUE(compose_future.Wait());
// Reset future for second compose call.
compose_future.Clear();
page_handler()->Compose("a user typed that", false);
// Ensure compose is finished before calling undo
EXPECT_TRUE(compose_future.Wait());
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.";
// This take should clear the quality future from the model that was undone.
std::unique_ptr<optimization_guide::ModelQualityLogEntry> result =
quality_test_future.Take();
EXPECT_EQ(
base::ScopedMockElapsedTimersForTest::kMockElapsedTime.InMilliseconds(),
result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->request_latency_ms());
// Close UI to submit remaining quality logs.
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
result = quality_test_future.Take();
EXPECT_EQ(
base::ScopedMockElapsedTimersForTest::kMockElapsedTime.InMilliseconds(),
result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->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);
base::test::TestFuture<
std::unique_ptr<optimization_guide::ModelQualityLogEntry>>
quality_test_future;
base::test::TestFuture<
std::unique_ptr<optimization_guide::ModelQualityLogEntry>>
quality_test_future_2;
EXPECT_CALL(model_quality_logs_uploader(), UploadModelQualityLogs(_))
.WillRepeatedly(testing::Invoke(
[&](std::unique_ptr<optimization_guide::ModelQualityLogEntry>
response) {
if (!quality_test_future.IsReady()) {
quality_test_future.SetValue(std::move(response));
} else {
quality_test_future_2.SetValue(std::move(response));
}
}));
page_handler()->Compose("a user typed this", false);
EXPECT_TRUE(compose_future.Wait()); // Reset future for second compose call.
compose_future.Clear();
page_handler()->Compose("a user typed that", false);
EXPECT_TRUE(compose_future.Wait());
// Close UI to submit remaining quality logs.
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
// This take should clear the quality future from the model that was undone.
std::unique_ptr<optimization_guide::ModelQualityLogEntry> result =
quality_test_future.Take();
EXPECT_EQ(optimization_guide::proto::FinalStatus::STATUS_ABANDONED,
result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->final_status());
result = quality_test_future_2.Take();
EXPECT_EQ(optimization_guide::proto::FinalStatus::STATUS_UNSPECIFIED,
result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->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<
std::unique_ptr<optimization_guide::ModelQualityLogEntry>>
quality_test_future;
EXPECT_CALL(model_quality_logs_uploader(), UploadModelQualityLogs(_))
.WillRepeatedly(testing::Invoke(
[&](std::unique_ptr<optimization_guide::ModelQualityLogEntry>
response) {
quality_test_future.SetValue(std::move(response));
}));
ShowDialogAndBindMojo();
client().GetSessionForActiveComposeField()->SetAllowFeedbackForTesting(true);
page_handler()->Compose("a user typed this", 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
std::unique_ptr<optimization_guide::ModelQualityLogEntry> result =
quality_test_future.Take();
EXPECT_EQ(optimization_guide::proto::UserFeedback::USER_FEEDBACK_THUMBS_UP,
result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->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<
std::unique_ptr<optimization_guide::ModelQualityLogEntry>>
quality_test_future;
EXPECT_CALL(model_quality_logs_uploader(), UploadModelQualityLogs(_))
.WillRepeatedly(testing::Invoke(
[&](std::unique_ptr<optimization_guide::ModelQualityLogEntry>
response) {
quality_test_future.SetValue(std::move(response));
}));
ShowDialogAndBindMojo();
client().GetSessionForActiveComposeField()->SetAllowFeedbackForTesting(true);
page_handler()->Compose("a user typed this", 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
std::unique_ptr<optimization_guide::ModelQualityLogEntry> result =
quality_test_future.Take();
EXPECT_EQ(optimization_guide::proto::UserFeedback::USER_FEEDBACK_THUMBS_DOWN,
result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->user_feedback());
// 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);
base::test::TestFuture<
std::unique_ptr<optimization_guide::ModelQualityLogEntry>>
quality_test_future;
base::test::TestFuture<
std::unique_ptr<optimization_guide::ModelQualityLogEntry>>
quality_test_future_2;
EXPECT_CALL(model_quality_logs_uploader(), UploadModelQualityLogs(_))
.WillRepeatedly(testing::Invoke(
[&](std::unique_ptr<optimization_guide::ModelQualityLogEntry>
response) {
if (!quality_test_future.IsReady()) {
quality_test_future.SetValue(std::move(response));
} else {
quality_test_future_2.SetValue(std::move(response));
}
}));
page_handler()->Compose("a user typed this", false);
EXPECT_TRUE(compose_future.Wait()); // Reset future for second compose call.
compose_future.Clear();
page_handler()->Compose("a user typed that", true);
EXPECT_TRUE(compose_future.Wait());
// Close UI to submit remaining quality logs.
client_page_handler()->CloseUI(compose::mojom::CloseReason::kCloseButton);
// This take should clear the quality future from the model that was undone.
std::unique_ptr<optimization_guide::ModelQualityLogEntry> result =
quality_test_future.Take();
EXPECT_TRUE(result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->was_generated_via_edit());
result = quality_test_future_2.Take();
EXPECT_FALSE(result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->was_generated_via_edit());
histograms().ExpectBucketCount(compose::kComposeRequestReason,
compose::ComposeRequestReason::kUpdateRequest,
1);
EXPECT_EQ(optimization_guide::proto::FinalStatus::STATUS_UNSPECIFIED,
result->quality_data<optimization_guide::ComposeFeatureTypeMap>()
->final_status());
// 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));
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, false);
compose::mojom::ComposeResponsePtr result = test_future.Take();
EXPECT_EQ(compose::mojom::ComposeStatus::kOk, result->status);
EXPECT_EQ("Cucumbers", result->result);
page_handler()->Rewrite(nullptr);
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);
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::kDialogShown, 1);
histograms().ExpectBucketCount(
compose::kComposeSessionEventCounts,
compose::ComposeSessionEventTypes::kRetryClicked, 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::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));
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, 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::StyleModifiers::NewTone(compose::mojom::Tone::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);
page_handler()->Rewrite(
compose::mojom::StyleModifiers::NewTone(compose::mojom::Tone::kCasual));
result = test_future.Take();
histograms().ExpectBucketCount(
compose::kComposeRequestReason,
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::kDialogShown, 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);
// 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));
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, 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::StyleModifiers::NewLength(
compose::mojom::Length::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);
page_handler()->Rewrite(compose::mojom::StyleModifiers::NewLength(
compose::mojom::Length::kShorter));
result = test_future.Take();
histograms().ExpectBucketCount(
compose::kComposeRequestReason,
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::kDialogShown, 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);
// 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<optimization_guide::ModelQualityLogEntry>(
std::make_unique<
optimization_guide::proto::LogAiDataRequest>())));
})));
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", 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", 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", 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) {
// a no op.
})));
page_handler()->Compose("a user typed this", 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);
}
#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 the dialog showing 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