// 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 "ash/capture_mode/capture_mode_education_controller.h"
#include "ash/accelerators/accelerator_tracker.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/keyboard_shortcut_view.h"
#include "ash/system/toast/anchored_nudge.h"
#include "ash/system/toast/anchored_nudge_manager_impl.h"
#include "ash/test/ash_test_base.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
namespace ash {
namespace {
constexpr char kCaptureModeNudgeId[] = "kCaptureModeNudge";
constexpr char kNudgeTimeToActionWithin1m[] =
"Ash.NotifierFramework.Nudge.TimeToAction.Within1m";
constexpr char kNudgeTimeToActionWithin1h[] =
"Ash.NotifierFramework.Nudge.TimeToAction.Within1h";
constexpr char kNudgeTimeToActionWithinSession[] =
"Ash.NotifierFramework.Nudge.TimeToAction.WithinSession";
constexpr float kKeyboardImageWidth = 448;
constexpr int kNudgeWorkAreaSpacing = 8;
PrefService* GetPrefService() {
return Shell::Get()->session_controller()->GetActivePrefService();
}
void CancelNudge(const std::string& id) {
Shell::Get()->anchored_nudge_manager()->Cancel(id);
}
} // namespace
class CaptureModeEducationControllerTest : public AshTestBase {
public:
CaptureModeEducationControllerTest(const std::string& arm_name = "")
: AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
scoped_feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{features::kCaptureModeEducation,
{{"CaptureModeEducationParam", arm_name}}}},
/*disabled_features=*/{});
}
CaptureModeEducationControllerTest(
const CaptureModeEducationControllerTest&) = delete;
CaptureModeEducationControllerTest& operator=(
const CaptureModeEducationControllerTest&) = delete;
~CaptureModeEducationControllerTest() override = default;
static void SetOverrideClock(base::Clock* test_clock) {
CaptureModeEducationController::SetOverrideClockForTesting(test_clock);
}
// By default, attempts to use the Windows Snipping Tool (capture bar)
// shortcut to activate the nudge.
void ActivateNudgeAndCheckVisibility(ui::KeyboardCode key_code = ui::VKEY_S,
int flags = ui::EF_COMMAND_DOWN |
ui::EF_SHIFT_DOWN) {
// Attempt to use the Windows Snipping Tool (capture bar) shortcut.
PressAndReleaseKey(key_code, flags);
// Get the list of visible nudges from the nudge manager and make sure our
// education nudge is in the list and visible.
const AnchoredNudge* nudge =
Shell::Get()->anchored_nudge_manager()->GetNudgeIfShown(
kCaptureModeNudgeId);
ASSERT_TRUE(nudge);
EXPECT_TRUE(nudge->GetVisible());
}
// Starts a capture session using the screenshot shortcut, and verifies that
// the nudge and tutorial are both closed. Stops the session afterwards.
void StartSessionAndCheckEducationClosed() {
// Use the screenshot shortcut to start a capture session.
PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1,
ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);
auto* capture_mode_controller = CaptureModeController::Get();
ASSERT_TRUE(capture_mode_controller->IsActive());
// Get the list of visible nudges from the nudge manager and make sure our
// education nudge is no longer in the list.
const AnchoredNudge* nudge =
Shell::Get()->anchored_nudge_manager()->GetNudgeIfShown(
kCaptureModeNudgeId);
EXPECT_FALSE(nudge);
// The tutorial should also be closed.
EXPECT_FALSE(capture_mode_controller->education_controller()
->tutorial_widget_for_test());
// Stop the session in case we start a new one later.
capture_mode_controller->Stop();
ASSERT_FALSE(capture_mode_controller->IsActive());
}
// Skip the 3 times/24 hours show limit for testing.
void SkipNudgePrefs() {
CaptureModeController::Get()->education_controller()->skip_prefs_for_test_ =
true;
}
protected:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(CaptureModeEducationControllerTest, Exists) {
// Check that the default arm is enabled.
ASSERT_EQ(features::kCaptureModeEducationParam.Get(),
features::CaptureModeEducationParam::kShortcutNudge);
ASSERT_TRUE(CaptureModeController::Get()->education_controller());
}
TEST_F(CaptureModeEducationControllerTest, NudgeAppearsOnAcceleratorPressed) {
base::SimpleTestClock test_clock;
CaptureModeEducationControllerTest::SetOverrideClock(&test_clock);
// We will need to show the nudge more than three times, so ignore pref
// limits.
SkipNudgePrefs();
for (auto [tracker_data, metadata] : kAcceleratorTrackerList) {
// We only want to test capture mode related misinputs.
if (metadata.type != TrackerType::kCaptureMode) {
continue;
}
ActivateNudgeAndCheckVisibility(tracker_data.key_code, tracker_data.flags);
// Close nudge to get ready for the next input.
CancelNudge(kCaptureModeNudgeId);
}
}
// Test fixture to verify the behaviour of Arm 1, the shortcut nudge.
class CaptureModeEducationShortcutNudgeTest
: public CaptureModeEducationControllerTest {
public:
CaptureModeEducationShortcutNudgeTest()
: CaptureModeEducationControllerTest("ShortcutNudge") {}
CaptureModeEducationShortcutNudgeTest(
const CaptureModeEducationShortcutNudgeTest&) = delete;
CaptureModeEducationShortcutNudgeTest& operator=(
const CaptureModeEducationShortcutNudgeTest&) = delete;
~CaptureModeEducationShortcutNudgeTest() override = default;
};
TEST_F(CaptureModeEducationShortcutNudgeTest, KeyboardShortcutVisible) {
base::SimpleTestClock test_clock;
CaptureModeEducationControllerTest::SetOverrideClock(&test_clock);
// Advance clock so we aren't at zero time.
test_clock.Advance(base::Hours(25));
// Attempt to use the Windows Snipping Tool (capture bar) shortcut.
PressAndReleaseKey(ui::VKEY_S, ui::EF_COMMAND_DOWN | ui::EF_SHIFT_DOWN);
// Get the list of visible nudges from the nudge manager and make sure our
// education nudge is in the list and visible.
AnchoredNudge* nudge =
Shell::Get()->anchored_nudge_manager()->GetNudgeIfShown(
kCaptureModeNudgeId);
ASSERT_TRUE(nudge);
EXPECT_TRUE(nudge->GetVisible());
// The keyboard shortcut view should be visible.
auto* shortcut_view = views::AsViewClass<KeyboardShortcutView>(
nudge->GetContentsView()->GetViewByID(
VIEW_ID_SYSTEM_NUDGE_SHORTCUT_VIEW));
ASSERT_TRUE(shortcut_view);
EXPECT_TRUE(shortcut_view->GetVisible());
StartSessionAndCheckEducationClosed();
CaptureModeEducationControllerTest::SetOverrideClock(nullptr);
}
TEST_F(CaptureModeEducationShortcutNudgeTest, EducationPreferencesShowLimit) {
base::SimpleTestClock test_clock;
CaptureModeEducationControllerTest::SetOverrideClock(&test_clock);
// Advance clock so we aren't at zero time.
test_clock.Advance(base::Hours(25));
AnchoredNudgeManagerImpl* nudge_manager =
Shell::Get()->anchored_nudge_manager();
for (int i = 0; i < 3; i++) {
ActivateNudgeAndCheckVisibility();
// Showing the nudge should also update the preferences.
EXPECT_EQ(
GetPrefService()->GetInteger(prefs::kCaptureModeEducationShownCount),
i + 1);
// Close the nudge.
CancelNudge(kCaptureModeNudgeId);
// Advance the clock so we can show the nudge again.
test_clock.Advance(base::Hours(25));
}
// Activate the nudge once more.
PressAndReleaseKey(ui::VKEY_S, ui::EF_COMMAND_DOWN | ui::EF_SHIFT_DOWN);
// The nudge should not be visible.
const AnchoredNudge* nudge =
nudge_manager->GetNudgeIfShown(kCaptureModeNudgeId);
ASSERT_FALSE(nudge);
// The nudge count should not increment as the nudge was not shown.
EXPECT_EQ(
GetPrefService()->GetInteger(prefs::kCaptureModeEducationShownCount), 3);
CaptureModeEducationControllerTest::SetOverrideClock(nullptr);
}
TEST_F(CaptureModeEducationShortcutNudgeTest, EducationPreferencesTimeLimit) {
base::SimpleTestClock test_clock;
CaptureModeEducationControllerTest::SetOverrideClock(&test_clock);
// Advance clock so we aren't at zero time.
test_clock.Advance(base::Hours(25));
ActivateNudgeAndCheckVisibility();
// Showing the nudge should also update the preferences.
EXPECT_EQ(
GetPrefService()->GetInteger(prefs::kCaptureModeEducationShownCount), 1);
// Close the nudge.
CancelNudge(kCaptureModeNudgeId);
// Advance the clock, but not more than 24 hours.
test_clock.Advance(base::Hours(20));
// Activate the nudge once more.
PressAndReleaseKey(ui::VKEY_S, ui::EF_COMMAND_DOWN | ui::EF_SHIFT_DOWN);
// The nudge should not be visible.
const AnchoredNudge* null_nudge =
Shell::Get()->anchored_nudge_manager()->GetNudgeIfShown(
kCaptureModeNudgeId);
ASSERT_FALSE(null_nudge);
// The nudge count should not increment as the nudge was not shown.
EXPECT_EQ(
GetPrefService()->GetInteger(prefs::kCaptureModeEducationShownCount), 1);
CaptureModeEducationControllerTest::SetOverrideClock(nullptr);
}
// Tests that metrics relating to the shortcut nudge (Arm 1) are properly
// recorded.
TEST_F(CaptureModeEducationShortcutNudgeTest, ShortcutNudgeMetrics) {
base::HistogramTester histogram_tester;
// For this test, we do not care about pref limits.
SkipNudgePrefs();
// The nudge has not been activated, so all related buckets should be at 0.
Shell::Get()->anchored_nudge_manager()->ResetNudgeRegistryForTesting();
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 0);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1h,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 0);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithinSession,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 0);
// The other nudge buckets should be empty as well.
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutTutorial, 0);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationQuickSettingsNudge, 0);
ActivateNudgeAndCheckVisibility();
// Attempt to use the screenshot shortcut as soon as possible.
PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1,
ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);
auto* capture_mode_controller = CaptureModeController::Get();
ASSERT_TRUE(capture_mode_controller->IsActive());
capture_mode_controller->Stop();
// The buckets do not cascade, so a nudge that has been activated within 1m
// will not show up in the `kNudgeTimeToActionWithin1h` bucket or session
// bucket.
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 1);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1h,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 0);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithinSession,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 0);
// The other nudge buckets should still be empty.
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutTutorial, 0);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationQuickSettingsNudge, 0);
ActivateNudgeAndCheckVisibility();
// Attempt to use the screenshot shortcut after more than 1 minute, but less
// than 1 hour.
task_environment()->FastForwardBy(base::Minutes(30));
PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1,
ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);
ASSERT_TRUE(capture_mode_controller->IsActive());
capture_mode_controller->Stop();
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 1);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1h,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 1);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithinSession,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 0);
ActivateNudgeAndCheckVisibility();
// Attempt to use the screenshot shortcut after more than 1 hour.
task_environment()->FastForwardBy(base::Hours(2));
PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1,
ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);
ASSERT_TRUE(capture_mode_controller->IsActive());
capture_mode_controller->Stop();
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 1);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1h,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 1);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithinSession,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 1);
}
// Test fixture to verify the behaviour of Arm 2, the shortcut tutorial.
class CaptureModeEducationShortcutTutorialTest
: public CaptureModeEducationControllerTest {
public:
CaptureModeEducationShortcutTutorialTest()
: CaptureModeEducationControllerTest("ShortcutTutorial") {}
CaptureModeEducationShortcutTutorialTest(
const CaptureModeEducationShortcutTutorialTest&) = delete;
CaptureModeEducationShortcutTutorialTest& operator=(
const CaptureModeEducationShortcutTutorialTest&) = delete;
~CaptureModeEducationShortcutTutorialTest() override = default;
};
TEST_F(CaptureModeEducationShortcutTutorialTest, DialogOpensAndCloses) {
base::SimpleTestClock test_clock;
CaptureModeEducationControllerTest::SetOverrideClock(&test_clock);
// Advance clock so we aren't at zero time.
test_clock.Advance(base::Hours(25));
// Attempt to use the Windows Snipping Tool (capture bar) shortcut.
PressAndReleaseKey(ui::VKEY_S, ui::EF_COMMAND_DOWN | ui::EF_SHIFT_DOWN);
// Click the "Show me how" button on the nudge.
AnchoredNudgeManagerImpl* nudge_manager =
Shell::Get()->anchored_nudge_manager();
LeftClickOn(nudge_manager->GetNudgePrimaryButtonForTest(kCaptureModeNudgeId));
// The nudge should not be visible.
EXPECT_FALSE(nudge_manager->GetNudgeIfShown(kCaptureModeNudgeId));
// The tutorial widget should be visible.
auto* tutorial_widget = CaptureModeController::Get()
->education_controller()
->tutorial_widget_for_test();
ASSERT_TRUE(tutorial_widget);
// The keyboard image should be scaled to the the correct width.
auto* image_view = tutorial_widget->GetContentsView()->GetViewByID(
VIEW_ID_SCREEN_CAPTURE_EDUCATION_KEYBOARD_IMAGE);
ASSERT_TRUE(image_view);
EXPECT_EQ(kKeyboardImageWidth, image_view->width());
StartSessionAndCheckEducationClosed();
}
// Tests that metrics relating to the shortcut tutorial (Arm 2) are properly
// recorded.
TEST_F(CaptureModeEducationShortcutTutorialTest, ShortcutTutorialMetrics) {
base::HistogramTester histogram_tester;
// For this test, we do not care about pref limits.
SkipNudgePrefs();
// The nudge has not been activated, so all related buckets should be at 0.
Shell::Get()->anchored_nudge_manager()->ResetNudgeRegistryForTesting();
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutTutorial, 0);
// The other nudge buckets should be empty as well.
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 0);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationQuickSettingsNudge, 0);
ActivateNudgeAndCheckVisibility();
// Attempt to use the screenshot shortcut as soon as possible.
PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1,
ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);
// The nudge bucket should now show a user action.
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutTutorial, 1);
// The other nudge buckets should still be empty.
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 0);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationQuickSettingsNudge, 0);
}
// Test fixture to verify the behaviour of Arm 3, the settings nudge.
class CaptureModeEducationQuickSettingsNudgeTest
: public CaptureModeEducationControllerTest {
public:
CaptureModeEducationQuickSettingsNudgeTest()
: CaptureModeEducationControllerTest("QuickSettingsNudge") {}
CaptureModeEducationQuickSettingsNudgeTest(
const CaptureModeEducationQuickSettingsNudgeTest&) = delete;
CaptureModeEducationQuickSettingsNudgeTest& operator=(
const CaptureModeEducationQuickSettingsNudgeTest&) = delete;
~CaptureModeEducationQuickSettingsNudgeTest() override = default;
};
TEST_F(CaptureModeEducationQuickSettingsNudgeTest, NudgeLocation) {
UpdateDisplay("800x600");
base::SimpleTestClock test_clock;
CaptureModeEducationControllerTest::SetOverrideClock(&test_clock);
// Advance clock so we aren't at zero time.
test_clock.Advance(base::Hours(25));
// Attempt to use the Windows Snipping Tool (capture bar) shortcut.
PressAndReleaseKey(ui::VKEY_S, ui::EF_COMMAND_DOWN | ui::EF_SHIFT_DOWN);
// Get the list of visible nudges from the nudge manager and make sure our
// education nudge is in the list and visible.
AnchoredNudge* nudge =
Shell::Get()->anchored_nudge_manager()->GetNudgeIfShown(
kCaptureModeNudgeId);
ASSERT_TRUE(nudge);
EXPECT_TRUE(nudge->GetVisible());
// Test the shelf in horizontal (bottom) alignment.
ASSERT_TRUE(Shell::GetPrimaryRootWindowController()
->shelf()
->IsHorizontalAlignment());
// The nudge should appear on the bottom right side of the screen. Compare the
// positions of the bottom right corners of the nudge and the work area.
const auto nudge_corner = nudge->GetBoundsInScreen().bottom_right();
auto work_area_corner =
nudge->GetWidget()->GetWorkAreaBoundsInScreen().bottom_right();
EXPECT_EQ(work_area_corner.x() - kNudgeWorkAreaSpacing, nudge_corner.x());
EXPECT_EQ(work_area_corner.y() - kNudgeWorkAreaSpacing, nudge_corner.y());
StartSessionAndCheckEducationClosed();
// TODO(hewer): Test shelf in vertical positions.
CaptureModeEducationControllerTest::SetOverrideClock(nullptr);
}
// Tests that metrics relating to the settings nudge (Arm 3) are properly
// recorded.
TEST_F(CaptureModeEducationQuickSettingsNudgeTest, SettingsNudgeMetrics) {
base::HistogramTester histogram_tester;
// For this test, we do not care about pref limits.
SkipNudgePrefs();
// The nudge has not been activated, so all related buckets should be at 0.
Shell::Get()->anchored_nudge_manager()->ResetNudgeRegistryForTesting();
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationQuickSettingsNudge, 0);
// The other nudge buckets should be empty as well.
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 0);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutTutorial, 0);
ActivateNudgeAndCheckVisibility();
// Attempt to use the screenshot shortcut as soon as possible.
PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1,
ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationQuickSettingsNudge, 1);
// The other nudge buckets should still be empty.
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 0);
histogram_tester.ExpectBucketCount(
kNudgeTimeToActionWithin1m,
NudgeCatalogName::kCaptureModeEducationShortcutTutorial, 0);
}
class CaptureModeEducationControllerBypassLimitsFlagTest
: public CaptureModeEducationControllerTest {
public:
CaptureModeEducationControllerBypassLimitsFlagTest() {
scoped_feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{features::kCaptureModeEducation,
{{"CaptureModeEducationParam", "ShortcutNudge"}}},
{features::kCaptureModeEducationBypassLimits, {}}},
/*disabled_features=*/{});
}
CaptureModeEducationControllerBypassLimitsFlagTest(
const CaptureModeEducationControllerBypassLimitsFlagTest&) = delete;
CaptureModeEducationControllerBypassLimitsFlagTest& operator=(
const CaptureModeEducationControllerBypassLimitsFlagTest&) = delete;
~CaptureModeEducationControllerBypassLimitsFlagTest() override = default;
protected:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(CaptureModeEducationControllerBypassLimitsFlagTest, NoShowLimits) {
// Show the nudge more than three times without advancing the clock,
// it should be visible each time.
for (int i = 0; i < 4; i++) {
ActivateNudgeAndCheckVisibility();
CancelNudge(kCaptureModeNudgeId);
}
}
} // namespace ash