// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <vector>
#include "ash/accelerators/keyboard_code_util.h"
#include "ash/accessibility/magnifier/docked_magnifier_controller.h"
#include "ash/annotator/annotations_overlay_controller.h"
#include "ash/annotator/annotator_controller.h"
#include "ash/capture_mode/capture_mode_bar_view.h"
#include "ash/capture_mode/capture_mode_constants.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_demo_tools_controller.h"
#include "ash/capture_mode/capture_mode_demo_tools_test_api.h"
#include "ash/capture_mode/capture_mode_menu_toggle_button.h"
#include "ash/capture_mode/capture_mode_metrics.h"
#include "ash/capture_mode/capture_mode_session.h"
#include "ash/capture_mode/capture_mode_session_focus_cycler.h"
#include "ash/capture_mode/capture_mode_session_test_api.h"
#include "ash/capture_mode/capture_mode_settings_test_api.h"
#include "ash/capture_mode/capture_mode_test_util.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/capture_mode/key_combo_view.h"
#include "ash/capture_mode/pointer_highlight_layer.h"
#include "ash/capture_mode/video_recording_watcher.h"
#include "ash/constants/ash_features.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/public/cpp/capture_mode/capture_mode_test_api.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_app_button.h"
#include "ash/shelf/shelf_test_util.h"
#include "ash/shelf/shelf_view.h"
#include "ash/shelf/shelf_view_test_api.h"
#include "ash/shell.h"
#include "ash/style/icon_button.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_util.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/timer/timer.h"
#include "chromeos/ui/base/window_state_type.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/ime/fake_text_input_client.h"
#include "ui/base/ime/text_input_type.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/pointer_details.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/controls/image_view.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
constexpr ui::KeyboardCode kIconKeyCodes[] = {ui::VKEY_BROWSER_BACK,
ui::VKEY_BROWSER_FORWARD,
ui::VKEY_BROWSER_REFRESH,
ui::VKEY_ZOOM,
ui::VKEY_MEDIA_LAUNCH_APP1,
ui::VKEY_BRIGHTNESS_DOWN,
ui::VKEY_BRIGHTNESS_UP,
ui::VKEY_VOLUME_MUTE,
ui::VKEY_VOLUME_DOWN,
ui::VKEY_VOLUME_UP,
ui::VKEY_UP,
ui::VKEY_DOWN,
ui::VKEY_LEFT,
ui::VKEY_RIGHT};
} // namespace
class CaptureModeDemoToolsTest : public AshTestBase {
public:
CaptureModeDemoToolsTest() = default;
CaptureModeDemoToolsTest(const CaptureModeDemoToolsTest&) = delete;
CaptureModeDemoToolsTest& operator=(const CaptureModeDemoToolsTest&) = delete;
~CaptureModeDemoToolsTest() override = default;
// AshTestBase:
void SetUp() override {
AshTestBase::SetUp();
window_ = CreateTestWindow(gfx::Rect(20, 30, 601, 300));
// Focus on non-input-text field at beginning.
fake_text_input_client_ =
std::make_unique<ui::FakeTextInputClient>(ui::TEXT_INPUT_TYPE_NONE);
}
void TearDown() override {
window_.reset();
AshTestBase::TearDown();
}
aura::Window* window() const { return window_.get(); }
gfx::Rect GetConfineBoundsInScreenCoordinates() {
auto* recording_watcher =
CaptureModeController::Get()->video_recording_watcher_for_testing();
gfx::Rect confine_bounds_in_screen =
recording_watcher->GetCaptureSurfaceConfineBounds();
wm::ConvertRectToScreen(recording_watcher->window_being_recorded(),
&confine_bounds_in_screen);
return confine_bounds_in_screen;
}
// Verifies that the `key_combo_widget` is positioned in the middle
// horizontally within the confine bounds and that the distance between the
// bottom of the widget and the bottom of the confine bounds is always equal
// to `capture_mode::kKeyWidgetDistanceFromBottom`.
void VerifyKeyComboWidgetPosition() {
CaptureModeDemoToolsTestApi demo_tools_test_api(
GetCaptureModeDemoToolsController());
auto* key_combo_widget = demo_tools_test_api.GetKeyComboWidget();
ASSERT_TRUE(key_combo_widget);
auto confine_bounds_in_screen = GetConfineBoundsInScreenCoordinates();
const gfx::Rect key_combo_widget_bounds =
key_combo_widget->GetWindowBoundsInScreen();
EXPECT_NEAR(confine_bounds_in_screen.CenterPoint().x(),
key_combo_widget_bounds.CenterPoint().x(), /*abs_error=*/1);
EXPECT_EQ(
confine_bounds_in_screen.bottom() - key_combo_widget_bounds.bottom(),
capture_mode::kKeyWidgetDistanceFromBottom);
}
IconButton* GetSettingsButton() const {
return GetCaptureModeBarView()->settings_button();
}
views::Widget* GetCaptureModeSettingsWidget() const {
auto* session = CaptureModeController::Get()->capture_mode_session();
DCHECK(session);
return CaptureModeSessionTestApi(session).GetCaptureModeSettingsWidget();
}
CaptureModeDemoToolsController* GetCaptureModeDemoToolsController() const {
auto* recording_watcher =
CaptureModeController::Get()->video_recording_watcher_for_testing();
DCHECK(recording_watcher);
return recording_watcher->demo_tools_controller_for_testing();
}
void WaitForMouseHighlightAnimationCompleted() {
base::RunLoop run_loop;
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
DCHECK(demo_tools_controller);
CaptureModeDemoToolsTestApi capture_mode_demo_tools_test_api(
demo_tools_controller);
capture_mode_demo_tools_test_api.SetOnMouseHighlightAnimationEndedCallback(
run_loop.QuitClosure());
run_loop.Run();
}
// Fires the key combo viewer timer and verifies the existence of the widget
// after the timer expires.
void FireTimerAndVerifyWidget(bool should_hide_view) {
auto* demo_tools_controller = GetCaptureModeDemoToolsController();
DCHECK(demo_tools_controller);
CaptureModeDemoToolsTestApi capture_mode_demo_tools_test_api(
demo_tools_controller);
auto* timer = capture_mode_demo_tools_test_api.GetRefreshKeyComboTimer();
EXPECT_TRUE(timer->IsRunning());
EXPECT_EQ(timer->GetCurrentDelay(),
should_hide_view
? capture_mode::kRefreshKeyComboWidgetLongDelay
: capture_mode::kRefreshKeyComboWidgetShortDelay);
KeyComboView* key_combo_view =
capture_mode_demo_tools_test_api.GetKeyComboView();
ViewVisibilityChangeWaiter waiter(key_combo_view);
timer->FireNow();
if (should_hide_view) {
waiter.Wait();
EXPECT_FALSE(capture_mode_demo_tools_test_api.GetKeyComboWidget());
EXPECT_FALSE(capture_mode_demo_tools_test_api.GetKeyComboView());
}
}
void EnableTextInputFocus(ui::TextInputType input_type) {
fake_text_input_client_->set_text_input_type(input_type);
Shell::Get()
->window_tree_host_manager()
->input_method()
->SetFocusedTextInputClient(fake_text_input_client_.get());
}
void DisableTextInputFocus() {
fake_text_input_client_->set_text_input_type(ui::TEXT_INPUT_TYPE_NONE);
Shell::Get()
->window_tree_host_manager()
->input_method()
->SetFocusedTextInputClient(nullptr);
}
void DragTouchAndVerifyHighlight(const ui::PointerId& touch_id,
const gfx::Point& touch_point,
const gfx::Vector2d& drag_offset) {
auto* event_generator = GetEventGenerator();
event_generator->PressTouchId(touch_id, touch_point);
CaptureModeDemoToolsTestApi demo_tools_test_api(
GetCaptureModeDemoToolsController());
const auto& touch_highlight_map =
demo_tools_test_api.GetTouchIdToHighlightLayerMap();
const auto iter =
touch_highlight_map.find(static_cast<ui::PointerId>(touch_id));
ASSERT_TRUE(iter != touch_highlight_map.end());
const auto* touch_highlight = iter->second.get();
auto original_touch_highlight_bounds = touch_highlight->layer()->bounds();
auto* recording_watcher =
CaptureModeController::Get()->video_recording_watcher_for_testing();
wm::ConvertRectToScreen(recording_watcher->window_being_recorded(),
&original_touch_highlight_bounds);
event_generator->MoveTouchBy(drag_offset.x(), drag_offset.y());
gfx::PointF updated_event_location{
event_generator->current_screen_location()};
const auto expected_touch_highlight_layer_bounds =
capture_mode_util::CalculateHighlightLayerBounds(
updated_event_location,
capture_mode::kHighlightLayerRadius +
capture_mode::kInnerHightlightBorderThickness +
capture_mode::kOuterHightlightBorderThickness);
auto actual_touch_highlight_layer_bounds = original_touch_highlight_bounds;
actual_touch_highlight_layer_bounds.Offset(drag_offset.x(),
drag_offset.y());
EXPECT_EQ(expected_touch_highlight_layer_bounds,
actual_touch_highlight_layer_bounds);
}
private:
std::unique_ptr<aura::Window> window_;
std::unique_ptr<ui::FakeTextInputClient> fake_text_input_client_;
};
// Tests that the key event is considered to generate the `key_combo_widget_`
// or ignored otherwise in a correct way.
TEST_F(CaptureModeDemoToolsTest, ConsiderKeyEvent) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
auto* event_generator = GetEventGenerator();
ClickOnView(GetSettingsButton(), event_generator);
EXPECT_TRUE(GetCaptureModeSettingsWidget());
Switch* toggle_button = CaptureModeSettingsTestApi()
.GetDemoToolsMenuToggleButton()
->toggle_button();
// The toggle button will be disabled by default, toggle the toggle button to
// enable the demo tools feature.
EXPECT_FALSE(toggle_button->GetIsOn());
ClickOnView(toggle_button, event_generator);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
// Press the 'A' key and the event will not be considered to generate a
// corresponding key widget.
event_generator->PressKey(ui::VKEY_A, ui::EF_NONE);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
EXPECT_FALSE(demo_tools_test_api.GetKeyComboWidget());
event_generator->ReleaseKey(ui::VKEY_A, ui::EF_NONE);
EXPECT_EQ(demo_tools_test_api.GetCurrentModifiersFlags(), 0);
EXPECT_EQ(demo_tools_test_api.GetLastNonModifierKey(), ui::VKEY_UNKNOWN);
// Press 'Ctrl' + 'A' and the key event will be considered to generate a
// corresponding key widget.
event_generator->PressKey(ui::VKEY_A, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
EXPECT_TRUE(demo_tools_test_api.GetKeyComboWidget());
EXPECT_EQ(demo_tools_test_api.GetCurrentModifiersFlags(),
ui::EF_CONTROL_DOWN);
EXPECT_EQ(demo_tools_test_api.GetLastNonModifierKey(), ui::VKEY_A);
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
base::OneShotTimer* timer = demo_tools_test_api.GetRefreshKeyComboTimer();
EXPECT_TRUE(timer->IsRunning());
timer->FireNow();
EXPECT_FALSE(demo_tools_test_api.GetKeyComboWidget());
EXPECT_EQ(demo_tools_test_api.GetCurrentModifiersFlags(), 0);
event_generator->ReleaseKey(ui::VKEY_A, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_TAB, ui::EF_NONE);
EXPECT_TRUE(demo_tools_test_api.GetKeyComboWidget());
EXPECT_EQ(demo_tools_test_api.GetCurrentModifiersFlags(), 0);
EXPECT_EQ(demo_tools_test_api.GetLastNonModifierKey(), ui::VKEY_TAB);
}
// Tests that the capture mode demo tools feature will be enabled if the
// toggle button is enabled and disabled otherwise.
TEST_F(CaptureModeDemoToolsTest, EntryPointTest) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
auto* event_generator = GetEventGenerator();
ClickOnView(GetSettingsButton(), event_generator);
EXPECT_TRUE(GetCaptureModeSettingsWidget());
Switch* toggle_button = CaptureModeSettingsTestApi()
.GetDemoToolsMenuToggleButton()
->toggle_button();
// The toggle button will be disabled by default.
EXPECT_FALSE(toggle_button->GetIsOn());
// Toggle the demo tools toggle button to enable the feature and start the
// video recording. The modifier key down event will be handled and the key
// combo viewer widget will be displayed.
EXPECT_TRUE(GetCaptureModeSettingsWidget());
ClickOnView(toggle_button, event_generator);
EXPECT_TRUE(toggle_button->GetIsOn());
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
EXPECT_TRUE(demo_tools_test_api.GetKeyComboWidget());
controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);
WaitForCaptureFileToBeSaved();
EXPECT_FALSE(controller->IsActive());
// Start another capture mode session and the demo tools toggle button will be
// enabled. Toggle the toggle button to disable the feature. The modifier key
// down event will not be handled when video recording starts.
controller = StartCaptureSession(CaptureModeSource::kFullscreen,
CaptureModeType::kVideo);
ClickOnView(GetSettingsButton(), event_generator);
EXPECT_TRUE(GetCaptureModeSettingsWidget());
toggle_button = CaptureModeSettingsTestApi()
.GetDemoToolsMenuToggleButton()
->toggle_button();
EXPECT_TRUE(toggle_button->GetIsOn());
ClickOnView(toggle_button, event_generator);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
EXPECT_FALSE(GetCaptureModeDemoToolsController());
}
// Tests that the demo tools button is navigated and toggled correctly with
// keyboard in the settings menu.
TEST_F(CaptureModeDemoToolsTest, EntryPointFocusCyclerTest) {
auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen,
CaptureModeType::kVideo);
auto* event_generator = GetEventGenerator();
using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup;
CaptureModeSessionTestApi session_test_api(
controller->capture_mode_session());
// Check the initial focus of the focus ring.
EXPECT_EQ(FocusGroup::kNone, session_test_api.GetCurrentFocusGroup());
// Tab 6 times to reach the settings button.
SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/6);
EXPECT_EQ(FocusGroup::kSettingsClose,
session_test_api.GetCurrentFocusGroup());
EXPECT_TRUE(CaptureModeSessionFocusCycler::HighlightHelper::Get(
session_test_api.GetCaptureModeBarView()->settings_button())
->has_focus());
// Press the space key and the settings menu will be opened.
SendKey(ui::VKEY_SPACE, event_generator, ui::EF_NONE);
EXPECT_TRUE(session_test_api.GetCaptureModeSettingsView());
EXPECT_EQ(FocusGroup::kPendingSettings,
session_test_api.GetCurrentFocusGroup());
// Tab once to enter focus into the settings menu.
SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE);
ASSERT_EQ(FocusGroup::kSettingsMenu, session_test_api.GetCurrentFocusGroup());
// Tab until focus reaches the demo tools toggle button.
Switch* toggle_button = CaptureModeSettingsTestApi()
.GetDemoToolsMenuToggleButton()
->toggle_button();
while (session_test_api.GetCurrentFocusedView()->GetView() != toggle_button) {
SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE);
}
// The demo tools toggle button will be disabled by default.
EXPECT_FALSE(toggle_button->GetIsOn());
// Press the space key to enable the toggle button.
SendKey(ui::VKEY_SPACE, event_generator, ui::EF_NONE);
EXPECT_TRUE(toggle_button->GetIsOn());
// Press the escape key and the focus will return to the settings button.
SendKey(ui::VKEY_ESCAPE, event_generator, ui::EF_NONE);
EXPECT_EQ(FocusGroup::kSettingsClose,
session_test_api.GetCurrentFocusGroup());
EXPECT_TRUE(CaptureModeSessionFocusCycler::HighlightHelper::Get(
session_test_api.GetCaptureModeBarView()->settings_button())
->has_focus());
}
// Tests that the demo tools toggle button will be hidden when starting another
// capture mode session during video recording.
TEST_F(CaptureModeDemoToolsTest, ToggleButtonHiddenWhileInRecording) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
auto* event_generator = GetEventGenerator();
ClickOnView(GetSettingsButton(), event_generator);
EXPECT_TRUE(GetCaptureModeSettingsWidget());
EXPECT_TRUE(CaptureModeSettingsTestApi().GetDemoToolsMenuToggleButton());
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
controller->Start(CaptureModeEntryType::kQuickSettings);
ClickOnView(GetSettingsButton(), event_generator);
EXPECT_TRUE(GetCaptureModeSettingsWidget());
EXPECT_FALSE(CaptureModeSettingsTestApi().GetDemoToolsMenuToggleButton());
}
// Tests that the key combo viewer widget displays the expected contents on key
// event and the modifier key should always be displayed before the non-modifier
// key. With no modifier keys or no non-modifier key that can be displayed
// independently, the key combo widget will not be displayed.
TEST_F(CaptureModeDemoToolsTest, KeyComboWidgetTest) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
auto* event_generator = GetEventGenerator();
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_C, ui::EF_NONE);
EXPECT_TRUE(demo_tools_test_api.GetKeyComboWidget());
EXPECT_TRUE(demo_tools_test_api.GetKeyComboView());
std::vector<ui::KeyboardCode> expected_modifier_key_vector = {
ui::VKEY_CONTROL};
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
expected_modifier_key_vector);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_C);
// Press the key 'Shift' at last, but it will still show before the 'C' key.
event_generator->PressKey(ui::VKEY_SHIFT, ui::EF_NONE);
expected_modifier_key_vector = {ui::VKEY_CONTROL, ui::VKEY_SHIFT};
EXPECT_TRUE(demo_tools_test_api.GetShownModifiersKeyCodes() ==
expected_modifier_key_vector);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_C);
// Release the modifier keys, and the key combo view will hide after the
// refresh timer expires.
event_generator->ReleaseKey(ui::VKEY_SHIFT, ui::EF_NONE);
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
FireTimerAndVerifyWidget(/*should_hide_view=*/true);
EXPECT_FALSE(demo_tools_test_api.GetKeyComboWidget());
}
// Tests the timer behaviors for the key combo view:
// 1. The refresh timer will be triggered on key up of the non-modifier key with
// no modifier keys pressed, the key combo view will hide after the timer
// expires;
// 2. The refresh timer will also be triggered on key up of the last modifier
// key with no non-modifier key that can be displayed independently pressed. The
// key combo view will hide after the timer expires;
// 3. If there is another key down event happens before the timer expires, the
// refresh timer stops and the key combo view will be updated to match the
// current keys pressed;
// 4. On key up while the refresh timer is still running, the key combo view
// will stay visible even the key states have been updated until the timer
// expires.
TEST_F(CaptureModeDemoToolsTest, DemoToolsTimerTest) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
// Press the 'Ctrl' + 'A' and verify the shown key widgets.
auto* event_generator = GetEventGenerator();
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_A, ui::EF_NONE);
EXPECT_TRUE(demo_tools_test_api.GetKeyComboWidget());
KeyComboView* key_combo_view = demo_tools_test_api.GetKeyComboView();
EXPECT_TRUE(key_combo_view);
std::vector<ui::KeyboardCode> expected_modifier_key_vector = {
ui::VKEY_CONTROL};
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
expected_modifier_key_vector);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_A);
// Release the non-modifier key and the timer with a delay of
// `capture_mode::kRefreshKeyComboWidgetShortDelay` will be triggered, the key
// combo view will be updated to show 'Ctrl'.
event_generator->ReleaseKey(ui::VKEY_A, ui::EF_NONE);
FireTimerAndVerifyWidget(/*should_hide_view=*/false);
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
expected_modifier_key_vector);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_UNKNOWN);
// Release the non-modifier key with no modifier keys pressed and the hide
// timer will be triggered.
event_generator->PressKey(ui::VKEY_A, ui::EF_NONE);
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->ReleaseKey(ui::VKEY_A, ui::EF_NONE);
FireTimerAndVerifyWidget(/*should_hide_view=*/true);
// Press 'Ctrl' + 'A' and release the only modifier key 'Ctrl' and
// the refresh timer will be triggered. The entire key combo viewer will hide
// after the refresh timer expires.
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_A, ui::EF_NONE);
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
FireTimerAndVerifyWidget(/*should_hide_view=*/true);
event_generator->ReleaseKey(ui::VKEY_A, ui::EF_NONE);
// Press 'Ctrl' + 'Shift' + 'A', then release 'A', the timer with a delay of
// `capture_mode::kRefreshKeyComboWidgetShortDelay` will be triggered. Press
// 'B' and the key combo view will be updated accordingly, i.e. 'Ctrl' +
// 'Shift' + 'B'.
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_SHIFT, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_A, ui::EF_NONE);
EXPECT_TRUE(demo_tools_test_api.GetKeyComboWidget());
expected_modifier_key_vector = {ui::VKEY_CONTROL, ui::VKEY_SHIFT};
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
expected_modifier_key_vector);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_A);
event_generator->ReleaseKey(ui::VKEY_A, ui::EF_NONE);
base::OneShotTimer* timer = demo_tools_test_api.GetRefreshKeyComboTimer();
EXPECT_TRUE(timer->IsRunning());
EXPECT_EQ(timer->GetCurrentDelay(),
capture_mode::kRefreshKeyComboWidgetShortDelay);
event_generator->PressKey(ui::VKEY_B, ui::EF_NONE);
EXPECT_FALSE(timer->IsRunning());
EXPECT_EQ(timer->GetCurrentDelay(),
capture_mode::kRefreshKeyComboWidgetShortDelay);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_B);
// Release the 'Ctrl' key, the timer with a delay of
// `capture_mode::kRefreshKeyComboWidgetShortDelay` will be triggered. Then
// release the 'Shift' key and the refresh timer will be triggered The entire
// key combo view will hide after the timer expires.
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
FireTimerAndVerifyWidget(/*should_hide_view=*/false);
expected_modifier_key_vector = {ui::VKEY_SHIFT};
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
expected_modifier_key_vector);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_B);
event_generator->ReleaseKey(ui::VKEY_SHIFT, ui::EF_NONE);
EXPECT_TRUE(timer->IsRunning());
EXPECT_EQ(timer->GetCurrentDelay(),
capture_mode::kRefreshKeyComboWidgetLongDelay);
event_generator->ReleaseKey(ui::VKEY_B, ui::EF_NONE);
// The contents of the widget remains the same before the timer expires.
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
expected_modifier_key_vector);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_B);
// The state the controller has been updated.
EXPECT_EQ(demo_tools_test_api.GetCurrentModifiersFlags(), 0);
EXPECT_EQ(demo_tools_test_api.GetLastNonModifierKey(), ui::VKEY_UNKNOWN);
FireTimerAndVerifyWidget(/*should_hide_view=*/true);
}
// Tests that all the non-modifier keys with the icon are displayed
// independently and correctly.
TEST_F(CaptureModeDemoToolsTest, AllIconKeysTest) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
auto* event_generator = GetEventGenerator();
for (const auto key_code : kIconKeyCodes) {
event_generator->PressKey(key_code, ui::EF_NONE);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), key_code);
views::ImageView* icon = demo_tools_test_api.GetNonModifierKeyItemIcon();
ASSERT_TRUE(icon);
const auto image_model = icon->GetImageModel();
const gfx::VectorIcon* vector_icon = GetVectorIconForKeyboardCode(key_code);
EXPECT_EQ(std::string(vector_icon->name),
std::string(image_model.GetVectorIcon().vector_icon()->name));
event_generator->ReleaseKey(key_code, ui::EF_NONE);
}
}
// Tests that the key combo viewer widget will not show if the input
// field is currently focused and will display in a normal way when the focus is
// detached.
TEST_F(CaptureModeDemoToolsTest, DoNotShowKeyComboViewerInInputField) {
for (const auto input_type :
{ui::TEXT_INPUT_TYPE_TEXT, ui::TEXT_INPUT_TYPE_PASSWORD,
ui::TEXT_INPUT_TYPE_SEARCH, ui::TEXT_INPUT_TYPE_EMAIL,
ui::TEXT_INPUT_TYPE_NUMBER, ui::TEXT_INPUT_TYPE_TELEPHONE,
ui::TEXT_INPUT_TYPE_URL, ui::TEXT_INPUT_TYPE_DATE,
ui::TEXT_INPUT_TYPE_DATE_TIME, ui::TEXT_INPUT_TYPE_DATE_TIME_LOCAL,
ui::TEXT_INPUT_TYPE_MONTH, ui::TEXT_INPUT_TYPE_TIME,
ui::TEXT_INPUT_TYPE_WEEK, ui::TEXT_INPUT_TYPE_TEXT_AREA,
ui::TEXT_INPUT_TYPE_CONTENT_EDITABLE,
ui::TEXT_INPUT_TYPE_DATE_TIME_FIELD, ui::TEXT_INPUT_TYPE_NULL}) {
EnableTextInputFocus(input_type);
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
auto* event_generator = GetEventGenerator();
// With the input text focus enabled before the video recording, the
// key combo viewer will not display when pressing 'Ctrl' and 'T'.
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_T, ui::EF_NONE);
EXPECT_FALSE(demo_tools_test_api.GetKeyComboWidget());
EXPECT_FALSE(demo_tools_test_api.GetKeyComboView());
event_generator->ReleaseKey(ui::VKEY_T, ui::EF_NONE);
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
// Disable the input text focus, the key combo viewer will show when
// pressing 'Ctrl' and 'T' in a non-input-text field.
DisableTextInputFocus();
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_T, ui::EF_NONE);
EXPECT_TRUE(demo_tools_test_api.GetKeyComboWidget());
EXPECT_TRUE(demo_tools_test_api.GetKeyComboView());
event_generator->ReleaseKey(ui::VKEY_T, ui::EF_NONE);
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
FireTimerAndVerifyWidget(/*should_hide_view=*/true);
// Enable the text input focus during the recording, the key combo
// viewer will not display when pressing 'Ctrl' and 'T'.
EnableTextInputFocus(input_type);
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_T, ui::EF_NONE);
EXPECT_FALSE(demo_tools_test_api.GetKeyComboWidget());
EXPECT_FALSE(demo_tools_test_api.GetKeyComboView());
event_generator->ReleaseKey(ui::VKEY_T, ui::EF_NONE);
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);
WaitForCaptureFileToBeSaved();
}
}
// verifies that after any key release, if the remaining pressed keys are no
// longer displayable, the widget will be scheduled to hide after
// `capture_mode::kRefreshKeyComboWidgetLongDelay`.
TEST_F(CaptureModeDemoToolsTest, ReleaseAllKeysConsistencyTest) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
auto* event_generator = GetEventGenerator();
auto key_combo_generator = [&]() {
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_SHIFT, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_C, ui::EF_NONE);
};
key_combo_generator();
KeyComboView* key_combo_view = demo_tools_test_api.GetKeyComboView();
EXPECT_TRUE(key_combo_view);
// Release the modifier key 'Ctrl' to trigger the timer with a delay
// of `capture_mode::kRefreshKeyComboWidgetShortDelay`.
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
base::OneShotTimer* timer = demo_tools_test_api.GetRefreshKeyComboTimer();
EXPECT_TRUE(timer->IsRunning());
EXPECT_EQ(timer->GetCurrentDelay(),
capture_mode::kRefreshKeyComboWidgetShortDelay);
std::vector<ui::KeyboardCode> expected_modifier_key_vector = {
ui::VKEY_CONTROL, ui::VKEY_SHIFT};
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
expected_modifier_key_vector);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_C);
// Release the modifier key 'Shift' and the refresh timer will be triggered.
event_generator->ReleaseKey(ui::VKEY_SHIFT, ui::EF_NONE);
EXPECT_TRUE(timer->IsRunning());
EXPECT_EQ(timer->GetCurrentDelay(),
capture_mode::kRefreshKeyComboWidgetLongDelay);
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
expected_modifier_key_vector);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_C);
FireTimerAndVerifyWidget(/*should_hide_view=*/true);
// Key combo viewer update test.
key_combo_generator();
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
EXPECT_TRUE(timer->IsRunning());
EXPECT_EQ(timer->GetCurrentDelay(),
capture_mode::kRefreshKeyComboWidgetShortDelay);
timer->FireNow();
expected_modifier_key_vector = {ui::VKEY_SHIFT};
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
expected_modifier_key_vector);
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_C);
}
// Tests that when the key combo is 'modifier key' + 'non-modifier key that can
// be shown independently', on key up of either key, the key combo viewer should
// be updated to show the other key. When both keys are released, the refresh
// timer will be triggered.
TEST_F(CaptureModeDemoToolsTest,
ModifierAndIndependentlyShownNonModifierKeyComboTest) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
auto* event_generator = GetEventGenerator();
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(kIconKeyCodes[0], ui::EF_NONE);
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
std::vector<ui::KeyboardCode>{ui::VKEY_CONTROL});
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), kIconKeyCodes[0]);
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
FireTimerAndVerifyWidget(/*should_hide_view=*/false);
EXPECT_TRUE(demo_tools_test_api.GetShownModifiersKeyCodes().empty());
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), kIconKeyCodes[0]);
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->ReleaseKey(kIconKeyCodes[0], ui::EF_NONE);
FireTimerAndVerifyWidget(/*should_hide_view=*/false);
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
std::vector<ui::KeyboardCode>{ui::VKEY_CONTROL});
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_UNKNOWN);
event_generator->ReleaseKey(ui::VKEY_CONTROL, ui::EF_NONE);
FireTimerAndVerifyWidget(/*should_hide_view=*/true);
}
// Tests that if the width of the confine bounds is smaller than that of the
// preferred size of the key combo widget, the key combo widget will be shifted
// to the left. But the right edge of the key combo widget will always be to the
// left of the right edge of the capture surface confine bounds.
TEST_F(CaptureModeDemoToolsTest,
ConfineBoundsSizeSmallerThanPreferredSizeTest) {
auto* controller = CaptureModeController::Get();
const gfx::Rect capture_region(100, 200, 200, 50);
controller->SetUserCaptureRegion(capture_region, /*by_user=*/true);
StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
auto* event_generator = GetEventGenerator();
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_SHIFT, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_C, ui::EF_NONE);
auto* demo_tools_controller = GetCaptureModeDemoToolsController();
DCHECK(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
KeyComboView* key_combo_view = demo_tools_test_api.GetKeyComboView();
EXPECT_TRUE(key_combo_view);
const auto confine_bounds = controller->GetCaptureSurfaceConfineBounds();
EXPECT_LT(confine_bounds.width(),
key_combo_view->GetBoundsInScreen().width());
EXPECT_GT(confine_bounds.right(),
key_combo_view->GetBoundsInScreen().right());
}
// Tests that the key combo widget will be re-posisioned correctly on capture
// window bounds change.
TEST_F(CaptureModeDemoToolsTest, CaptureBoundsChangeTest) {
UpdateDisplay("800x700");
const auto window = CreateTestWindow(gfx::Rect(100, 150, 300, 500));
auto* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
EXPECT_EQ(split_view_controller->state(),
SplitViewController::State::kNoSnap);
auto* capture_mode_controller =
StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo);
auto* event_generator = GetEventGenerator();
event_generator->MoveMouseToCenterOf(window.get());
capture_mode_controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
EXPECT_TRUE(capture_mode_controller->is_recording_in_progress());
auto* demo_tools_controller = GetCaptureModeDemoToolsController();
DCHECK(demo_tools_controller);
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_C, ui::EF_NONE);
VerifyKeyComboWidgetPosition();
// Snap the `window` which will result in window bounds change and the key
// combo widget will still be centered horizontally.
const WindowSnapWMEvent event(WM_EVENT_SNAP_PRIMARY);
WindowState* window_state = WindowState::Get(window.get());
window_state->OnWMEvent(&event);
EXPECT_EQ(chromeos::WindowStateType::kPrimarySnapped,
window_state->GetStateType());
VerifyKeyComboWidgetPosition();
}
// Tests that there is no crash when work area changed after starting a video
// recording with demo tools enabled. Docked mananifier is used as an example to
// trigger the work area change.
TEST_F(CaptureModeDemoToolsTest, WorkAreaChangeTest) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
auto* docked_magnifier_controller =
Shell::Get()->docked_magnifier_controller();
docked_magnifier_controller->SetEnabled(/*enabled=*/true);
controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);
}
// Tests that if a touch down event happens before video recording starts, there
// will be no crash and no touch highlight will be generated.
TEST_F(CaptureModeDemoToolsTest, TouchDownBeforeVideoRecordingTest) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
// Press touch before starting the video recording.
auto* root_window = Shell::GetPrimaryRootWindow();
const gfx::Rect root_window_bounds_in_screen =
root_window->GetBoundsInScreen();
const gfx::Point display_center = root_window_bounds_in_screen.CenterPoint();
auto* event_generator = GetEventGenerator();
event_generator->PressTouchId(0, display_center);
StartVideoRecordingImmediately();
WaitForSeconds(1);
auto* demo_tools_controller = GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
const auto& touch_highlight_map =
demo_tools_test_api.GetTouchIdToHighlightLayerMap();
EXPECT_TRUE(touch_highlight_map.empty());
event_generator->ReleaseTouchId(0);
controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);
}
// Tests that the drag and drop in the shelf during video recording with demo
// tools enabled works properly with no crash.
TEST_F(CaptureModeDemoToolsTest, DragAndDropIconOnShelfTest) {
ShelfItem item = ShelfTestUtil::AddAppShortcut("app_id", TYPE_PINNED_APP);
const ShelfID& id = item.id;
ShelfView* shelf_view = GetPrimaryShelf()->GetShelfViewForTesting();
ShelfViewTestAPI test_api(shelf_view);
ShelfAppButton* button =
test_api.GetButton(ShelfModel::Get()->ItemIndexByID(id));
ASSERT_TRUE(button);
const gfx::Point button_center_point =
button->GetBoundsInScreen().CenterPoint();
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
auto* event_generator = GetEventGenerator();
event_generator->PressTouch(button_center_point);
ASSERT_TRUE(button->FireDragTimerForTest());
button->FireRippleActivationTimerForTest();
ui::GestureEventDetails event_details(ui::EventType::kGestureLongPress);
ui::GestureEvent long_press(button_center_point.x(), button_center_point.y(),
0, ui::EventTimeForNow(), event_details);
event_generator->Dispatch(&long_press);
event_generator->MoveTouchBy(0, -10);
EXPECT_TRUE(shelf_view->drag_view());
EXPECT_TRUE(button->state() & ShelfAppButton::STATE_DRAGGING);
event_generator->ReleaseTouch();
controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);
}
// Tests that the order of the currently pressed modifier keys will be preserved
// when updating the key combo view by removing released keys or appending new
// keys.
TEST_F(CaptureModeDemoToolsTest, FollowPreDeterminedOrder) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
std::vector<ui::KeyboardCode> expected_modifier_key_vector = {
ui::VKEY_CONTROL, ui::VKEY_MENU, ui::VKEY_SHIFT, ui::VKEY_COMMAND};
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
auto* event_generator = GetEventGenerator();
event_generator->PressKey(ui::VKEY_SHIFT, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_COMMAND, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_MENU, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
expected_modifier_key_vector);
event_generator->ReleaseKey(ui::VKEY_SHIFT, ui::EF_NONE);
FireTimerAndVerifyWidget(/*should_hide_view=*/false);
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
std::vector<ui::KeyboardCode>(
{ui::VKEY_CONTROL, ui::VKEY_MENU, ui::VKEY_COMMAND}));
event_generator->PressKey(ui::VKEY_SHIFT, ui::EF_NONE);
EXPECT_EQ(demo_tools_test_api.GetShownModifiersKeyCodes(),
expected_modifier_key_vector);
}
// Tests that if a new capture mode session gets triggered by keyboard shortcut
// while in video recording with demo tools on, the bounds of the key combo
// widget will be updated to avoid collision.
TEST_F(CaptureModeDemoToolsTest, KeyComboWidgetDeIntersectsWithCaptureBar) {
UpdateDisplay("800x700");
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
// Start a new capture mode session with keyboard shortcut.
PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1,
ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);
auto* key_combo_widget = demo_tools_test_api.GetKeyComboWidget();
EXPECT_TRUE(key_combo_widget);
const gfx::Rect original_bounds = key_combo_widget->GetWindowBoundsInScreen();
const auto* capture_bar_view = GetCaptureModeBarView();
EXPECT_TRUE(capture_bar_view);
const auto capture_bar_bounds = capture_bar_view->GetBoundsInScreen();
const int capture_bar_y = capture_bar_bounds.y();
EXPECT_LT(capture_bar_y, original_bounds.bottom());
PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1,
ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);
key_combo_widget = demo_tools_test_api.GetKeyComboWidget();
const gfx::Rect new_bounds = key_combo_widget->GetWindowBoundsInScreen();
EXPECT_GT(capture_bar_y, new_bounds.bottom());
}
// Tests that the auto click bar will be repositioned once there is a collision
// with the key combo widget.
TEST_F(CaptureModeDemoToolsTest, KeyComboWidgetDeIntersectsWithAutoClickBar) {
auto* autoclick_bubble_widget = EnableAndGetAutoClickBubbleWidget();
const gfx::Rect original_auto_click_widget_bounds =
autoclick_bubble_widget->GetWindowBoundsInScreen();
CaptureModeController* controller =
StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo);
// Intentionally create a `capture_region` within which the bounds of the key
// combo widget generated will mostly likely to collide with the
// `autoclick_bubble_widget`.
gfx::Rect capture_region = original_auto_click_widget_bounds;
capture_region.Inset(-16);
controller->SetUserCaptureRegion(capture_region,
/*by_user=*/true);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
auto* event_generator = GetEventGenerator();
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_C, ui::EF_NONE);
CaptureModeDemoToolsTestApi demo_tools_test_api(
GetCaptureModeDemoToolsController());
auto* key_combo_widget = demo_tools_test_api.GetKeyComboWidget();
ASSERT_TRUE(key_combo_widget);
const gfx::Rect key_combo_widget_bounds =
key_combo_widget->GetWindowBoundsInScreen();
EXPECT_TRUE(
key_combo_widget_bounds.Intersects(original_auto_click_widget_bounds));
const gfx::Rect new_autoclick_widget_bounds =
autoclick_bubble_widget->GetWindowBoundsInScreen();
EXPECT_FALSE(key_combo_widget_bounds.Intersects(new_autoclick_widget_bounds));
controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);
}
// Tests that the key combo viewer widget will display for key event coming from
// on-screen keyboard. For such key event, the key combo viewer will show on key
// down of a modifier key whose `flags()` is not 0 or non-modifier key that is
// allowed to show independently.
TEST_F(CaptureModeDemoToolsTest, OnScreenKeyboardKeyEventTest) {
CaptureModeController* controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
controller->EnableDemoTools(true);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
CaptureModeDemoToolsController* demo_tools_controller =
GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
auto* event_generator = GetEventGenerator();
PressAndReleaseKeyOnVK(event_generator, ui::VKEY_A, ui::EF_CONTROL_DOWN);
EXPECT_THAT(demo_tools_test_api.GetShownModifiersKeyCodes(),
testing::ElementsAre(ui::VKEY_CONTROL));
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_A);
FireTimerAndVerifyWidget(/*should_hide_view=*/true);
PressAndReleaseKeyOnVK(event_generator, ui::VKEY_TAB, ui::EF_NONE);
EXPECT_TRUE(demo_tools_test_api.GetShownModifiersKeyCodes().empty());
EXPECT_EQ(demo_tools_test_api.GetShownNonModifierKeyCode(), ui::VKEY_TAB);
FireTimerAndVerifyWidget(/*should_hide_view=*/true);
}
// Tests that the metrics that record if a recording starts with demo tools
// feature enabled are recorded correctly in a capture session both in clamshell
// and tablet mode.
TEST_F(CaptureModeDemoToolsTest,
DemoToolsEnabledOnRecordingStartHistogramTest) {
base::HistogramTester histogram_tester;
constexpr char kHistogramNameBase[] = "DemoToolsEnabledOnRecordingStart";
struct {
bool enable_tablet_mode;
bool enable_demo_tools;
} kTestCases[]{
{/*enable_tablet_mode=*/false, /*enable_demo_tools=*/false},
{/*enable_tablet_mode=*/false, /*enable_demo_tools=*/true},
{/*enable_tablet_mode=*/true, /*enable_demo_tools=*/false},
{/*enable_tablet_mode=*/true, /*enable_demo_tools=*/true},
};
for (const auto test_case : kTestCases) {
if (test_case.enable_tablet_mode) {
SwitchToTabletMode();
EXPECT_TRUE(Shell::Get()->IsInTabletMode());
} else {
EXPECT_FALSE(Shell::Get()->IsInTabletMode());
}
const auto histogram_name =
BuildHistogramName(kHistogramNameBase, /*behavior=*/nullptr,
/*append_ui_mode_suffix=*/true);
histogram_tester.ExpectBucketCount(histogram_name,
test_case.enable_demo_tools, 0);
auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen,
CaptureModeType::kVideo);
controller->EnableDemoTools(test_case.enable_demo_tools);
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);
WaitForCaptureFileToBeSaved();
histogram_tester.ExpectBucketCount(histogram_name,
test_case.enable_demo_tools, 1);
}
}
class CaptureModeDemoToolsTestWithAllSources
: public CaptureModeDemoToolsTest,
public testing::WithParamInterface<CaptureModeSource> {
public:
CaptureModeDemoToolsTestWithAllSources() = default;
CaptureModeDemoToolsTestWithAllSources(
const CaptureModeDemoToolsTestWithAllSources&) = delete;
CaptureModeDemoToolsTestWithAllSources& operator=(
const CaptureModeDemoToolsTestWithAllSources&) = delete;
~CaptureModeDemoToolsTestWithAllSources() override = default;
CaptureModeController* StartDemoToolsEnabledVideoRecordingWithParam() {
auto* controller = CaptureModeController::Get();
const gfx::Rect capture_region(100, 200, 300, 400);
controller->SetUserCaptureRegion(capture_region, /*by_user=*/true);
StartCaptureSession(GetParam(), CaptureModeType::kVideo);
controller->EnableDemoTools(true);
if (GetParam() == CaptureModeSource::kWindow)
GetEventGenerator()->MoveMouseToCenterOf(window());
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
return controller;
}
};
// Tests that the key combo viewer widget should be centered within its confine
// bounds.
TEST_P(CaptureModeDemoToolsTestWithAllSources,
KeyComboViewerShouldBeCenteredTest) {
auto* controller = StartDemoToolsEnabledVideoRecordingWithParam();
auto* demo_tools_controller = GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
auto* event_generator = GetEventGenerator();
const auto kKeyCodes = {ui::VKEY_CONTROL, ui::VKEY_SHIFT, ui::VKEY_A};
for (const auto key_code : kKeyCodes) {
event_generator->PressKey(key_code, ui::EF_NONE);
VerifyKeyComboWidgetPosition();
}
controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);
WaitForCaptureFileToBeSaved();
EXPECT_FALSE(controller->IsActive());
}
// Tests that the mouse highlight layer will be created on mouse down and
// will disappear after the animation.
TEST_P(CaptureModeDemoToolsTestWithAllSources, MouseHighlightTest) {
ui::ScopedAnimationDurationScaleMode normal_animation(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
StartDemoToolsEnabledVideoRecordingWithParam();
auto* demo_tools_controller = GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
gfx::Rect confine_bounds_in_screen = GetConfineBoundsInScreenCoordinates();
auto* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(confine_bounds_in_screen.CenterPoint());
event_generator->PressLeftButton();
event_generator->ReleaseLeftButton();
const MouseHighlightLayers& highlight_layers =
demo_tools_test_api.GetMouseHighlightLayers();
EXPECT_FALSE(highlight_layers.empty());
EXPECT_EQ(highlight_layers.size(), 1u);
WaitForMouseHighlightAnimationCompleted();
EXPECT_TRUE(highlight_layers.empty());
}
// Tests that multiple mouse highlight layers will be visible on consecutive
// mouse press events when the whole duration are within the expiration of the
// first animation expiration. It also tests that each mouse highlight layer
// will be centered on its mouse event location.
TEST_P(CaptureModeDemoToolsTestWithAllSources,
MouseHighlightShouldBeCenteredWithMouseClick) {
ui::ScopedAnimationDurationScaleMode normal_animation(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
StartDemoToolsEnabledVideoRecordingWithParam();
auto* recording_watcher =
CaptureModeController::Get()->video_recording_watcher_for_testing();
auto* window_being_recorded = recording_watcher->window_being_recorded();
auto* demo_tools_controller = GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api =
CaptureModeDemoToolsTestApi(demo_tools_controller);
gfx::Rect inner_rect = GetConfineBoundsInScreenCoordinates();
inner_rect.Inset(5);
const auto& layers_vector = demo_tools_test_api.GetMouseHighlightLayers();
auto* event_generator = GetEventGenerator();
for (const auto point : {inner_rect.CenterPoint(), inner_rect.origin(),
inner_rect.bottom_right()}) {
event_generator->MoveMouseTo(point);
event_generator->PressLeftButton();
event_generator->ReleaseLeftButton();
auto* highlight_layer = layers_vector.back().get();
auto highlight_center_point =
highlight_layer->layer()->bounds().CenterPoint();
// Convert the highlight layer center pointer to screen coordinates.
wm::ConvertPointToScreen(window_being_recorded, &highlight_center_point);
EXPECT_EQ(highlight_center_point, point);
}
EXPECT_EQ(layers_vector.size(), 3u);
}
// Tests that the key combo viewer is positioned correctly on device
// scale factor change.
TEST_P(CaptureModeDemoToolsTestWithAllSources, DeviceScaleFactorTest) {
StartDemoToolsEnabledVideoRecordingWithParam();
auto* demo_tools_controller = GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
auto* event_generator = GetEventGenerator();
event_generator->PressKey(ui::VKEY_CONTROL, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_SHIFT, ui::EF_NONE);
event_generator->PressKey(ui::VKEY_A, ui::EF_NONE);
const float kDeviceScaleFactors[] = {0.5f, 1.2f, 2.5f};
for (const float dsf : kDeviceScaleFactors) {
SetDeviceScaleFactor(dsf);
EXPECT_NEAR(dsf, window()->GetHost()->device_scale_factor(), 0.01);
VerifyKeyComboWidgetPosition();
}
}
// Tests that the touch highlight layer will be created on touch
// down and removed on touch up. It also tests that the bounds of the touch
// highlight layer will be updated correctly on the touch drag event.
TEST_P(CaptureModeDemoToolsTestWithAllSources, TouchHighlightTest) {
StartDemoToolsEnabledVideoRecordingWithParam();
auto* demo_tools_controller = GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
const gfx::Rect confine_bounds_in_screen =
GetConfineBoundsInScreenCoordinates();
auto* event_generator = GetEventGenerator();
const auto& touch_highlight_map =
demo_tools_test_api.GetTouchIdToHighlightLayerMap();
const auto center_point = confine_bounds_in_screen.CenterPoint();
event_generator->PressTouchId(0, center_point);
EXPECT_FALSE(touch_highlight_map.empty());
event_generator->ReleaseTouchId(0);
EXPECT_TRUE(touch_highlight_map.empty());
const gfx::Vector2d drag_offset =
gfx::Vector2d(confine_bounds_in_screen.width() / 4,
confine_bounds_in_screen.height() / 4);
DragTouchAndVerifyHighlight(/*touch_id=*/0, /*touch_point=*/center_point,
drag_offset);
}
// Tests the behaviors when multiple touches are performed.
// 1. The corresponding touch highlight will be generated on touch down;
// 2. The number of touch highlights kept in the demo tools controller is the
// same as the number of touch down events;
// 3. The bounds of the touch highlights will be updated correctly when dragging
// multiple touch events simultaneously;
// 4. The corresponding touch highlight will be removed on touch up. The
// number of touch highlights kept in the demo tools controller will become zero
// when all touches are released or cancelled.
TEST_P(CaptureModeDemoToolsTestWithAllSources, MutiTouchHighlightTest) {
StartDemoToolsEnabledVideoRecordingWithParam();
auto* demo_tools_controller = GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
const auto& touch_highlight_map =
demo_tools_test_api.GetTouchIdToHighlightLayerMap();
EXPECT_TRUE(touch_highlight_map.empty());
gfx::Rect inner_rect = GetConfineBoundsInScreenCoordinates();
inner_rect.Inset(20);
struct {
int touch_id;
gfx::Point touch_point;
gfx::Vector2d drag_offset;
} kTestCases[] = {
{/*touch_id=*/1, inner_rect.CenterPoint(), gfx::Vector2d(15, 25)},
{/*touch_id=*/0, inner_rect.origin(), gfx::Vector2d(10, -20)},
{/*touch_id=*/2, inner_rect.bottom_right(), gfx::Vector2d(-30, -20)}};
// Iterate through the kTestCases and perform the touch down. The
// corresponding touch highlight will be generated. Drag these touch events
// and check if the bounds of the corresponding touch highlight are updated
// correctly.
for (const auto& test_case : kTestCases) {
DragTouchAndVerifyHighlight(test_case.touch_id, test_case.touch_point,
test_case.drag_offset);
}
EXPECT_EQ(touch_highlight_map.size(), 3u);
// Release the touch event one by one and the corresponding touch highlight
// layer will be removed. The number of highlight layers kept in the demo
// tools controller will become zero when all touches are released or
// cancelled.
for (const auto& test_case : kTestCases) {
GetEventGenerator()->ReleaseTouchId(test_case.touch_id);
EXPECT_FALSE(touch_highlight_map.contains(
static_cast<ui::PointerId>(test_case.touch_id)));
}
EXPECT_TRUE(touch_highlight_map.empty());
}
INSTANTIATE_TEST_SUITE_P(All,
CaptureModeDemoToolsTestWithAllSources,
testing::Values(CaptureModeSource::kFullscreen,
CaptureModeSource::kRegion,
CaptureModeSource::kWindow));
class ProjectorCaptureModeDemoToolsTest : public CaptureModeDemoToolsTest {
public:
ProjectorCaptureModeDemoToolsTest() = default;
~ProjectorCaptureModeDemoToolsTest() override = default;
// CaptureModeDemoToolsTest:
void SetUp() override {
CaptureModeDemoToolsTest::SetUp();
projector_helper_.SetUp();
}
void StartProjectorModeSession() {
projector_helper_.StartProjectorModeSession();
}
private:
ProjectorCaptureModeIntegrationHelper projector_helper_;
};
// Tests that the demo tools feature will be enabled by default in a
// projector-initiated capture mode session and this overwritten configuration
// will not be carried over to a normal capture mode session.
TEST_F(ProjectorCaptureModeDemoToolsTest, EnableDemoToolsByDefault) {
CaptureModeController* capture_mode_controller = StartCaptureSession(
CaptureModeSource::kFullscreen, CaptureModeType::kVideo);
EXPECT_TRUE(capture_mode_controller->IsActive());
EXPECT_FALSE(capture_mode_controller->enable_demo_tools());
capture_mode_controller->Stop();
StartProjectorModeSession();
EXPECT_TRUE(capture_mode_controller->IsActive());
EXPECT_TRUE(capture_mode_controller->enable_demo_tools());
capture_mode_controller->Stop();
capture_mode_controller->Start(CaptureModeEntryType::kQuickSettings);
EXPECT_FALSE(capture_mode_controller->enable_demo_tools());
}
// Tests that the pointer (mouse and touch) highlight will be disabled when
// annotating and re-enabled after stopping the annotation in a
// projector-initiated capture mode.
TEST_F(ProjectorCaptureModeDemoToolsTest,
DisablePointerHighlightWithAnnotatorEnabled) {
ui::ScopedAnimationDurationScaleMode animation_scale(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
auto* capture_mode_controller = CaptureModeController::Get();
capture_mode_controller->SetSource(CaptureModeSource::kFullscreen);
StartProjectorModeSession();
EXPECT_TRUE(capture_mode_controller->enable_demo_tools());
StartVideoRecordingImmediately();
EXPECT_TRUE(capture_mode_controller->is_recording_in_progress());
auto* demo_tools_controller = GetCaptureModeDemoToolsController();
EXPECT_TRUE(demo_tools_controller);
CaptureModeDemoToolsTestApi demo_tools_test_api(demo_tools_controller);
const gfx::Rect confine_bounds_in_screen =
GetConfineBoundsInScreenCoordinates();
const gfx::Point center_point = confine_bounds_in_screen.CenterPoint();
auto* event_generator = GetEventGenerator();
auto mouse_highlight_test = [&](bool annotating) {
event_generator->MoveMouseTo(center_point);
event_generator->PressLeftButton();
event_generator->ReleaseLeftButton();
auto& mouse_highlight_layers =
demo_tools_test_api.GetMouseHighlightLayers();
if (annotating) {
EXPECT_TRUE(mouse_highlight_layers.empty());
} else {
EXPECT_FALSE(mouse_highlight_layers.empty());
}
};
auto touch_highlight_test = [&](bool annotating) {
event_generator->PressTouchId(0, center_point);
auto& touch_highlight_map =
demo_tools_test_api.GetTouchIdToHighlightLayerMap();
if (annotating) {
EXPECT_TRUE(touch_highlight_map.empty());
} else {
EXPECT_FALSE(touch_highlight_map.empty());
}
event_generator->ReleaseTouchId(0);
EXPECT_TRUE(touch_highlight_map.empty());
};
CaptureModeTestApi test_api;
AnnotationsOverlayController* annotations_overlay_controller =
test_api.GetAnnotationsOverlayController();
auto* annotator_controller = Shell::Get()->annotator_controller();
annotator_controller->EnableAnnotatorTool();
EXPECT_TRUE(annotations_overlay_controller->is_enabled());
mouse_highlight_test(/*annotating=*/true);
touch_highlight_test(/*annotating=*/true);
annotator_controller->ResetTools();
EXPECT_TRUE(capture_mode_controller->is_recording_in_progress());
EXPECT_FALSE(annotations_overlay_controller->is_enabled());
mouse_highlight_test(/*annotating=*/false);
touch_highlight_test(/*annotating=*/false);
}
// Tests that the metrics that record if a recording starts with demo tools
// feature enabled are recorded correctly in a projector-initiated capture
// session both in clamshell and tablet mode.
TEST_F(ProjectorCaptureModeDemoToolsTest,
ProjectorDemoToolsEnabledOnRecordingStartHistogramTest) {
base::HistogramTester histogram_tester;
constexpr char kHistogramNameBase[] = "DemoToolsEnabledOnRecordingStart";
struct {
bool enable_tablet_mode;
bool enable_demo_tools;
} kTestCases[]{
{/*enable_tablet_mode=*/false, /*enable_demo_tools=*/false},
{/*enable_tablet_mode=*/false, /*enable_demo_tools=*/true},
{/*enable_tablet_mode=*/true, /*enable_demo_tools=*/false},
{/*enable_tablet_mode=*/true, /*enable_demo_tools=*/true},
};
for (const auto test_case : kTestCases) {
if (test_case.enable_tablet_mode) {
SwitchToTabletMode();
EXPECT_TRUE(Shell::Get()->IsInTabletMode());
} else {
EXPECT_FALSE(Shell::Get()->IsInTabletMode());
}
const std::string histogram_name = BuildHistogramName(
kHistogramNameBase,
CaptureModeTestApi().GetBehavior(BehaviorType::kProjector),
/*append_ui_mode_suffix=*/true);
histogram_tester.ExpectBucketCount(histogram_name,
test_case.enable_demo_tools, 0);
auto* controller = CaptureModeController::Get();
controller->SetSource(CaptureModeSource::kFullscreen);
// Start a projector-initiated capture mode sesession, the demo tools
// feature will be enabled by default. `EnableDemoTools` to ensure that the
// test coverage includes both enabled and disabled cases.
StartProjectorModeSession();
EXPECT_TRUE(controller->enable_demo_tools());
controller->EnableDemoTools(test_case.enable_demo_tools);
EXPECT_TRUE(controller->IsActive());
StartVideoRecordingImmediately();
EXPECT_TRUE(controller->is_recording_in_progress());
WaitForSeconds(1);
controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton);
WaitForCaptureFileToBeSaved();
histogram_tester.ExpectBucketCount(
histogram_name, test_case.enable_demo_tools, /*expected_count=*/1);
}
}
} // namespace ash