// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include #include #include "ash/accessibility/magnifier/docked_magnifier_controller.h" #include "ash/accessibility/magnifier/magnifier_glass.h" #include "ash/app_list/app_list_controller_impl.h" #include "ash/capture_mode/capture_label_view.h" #include "ash/capture_mode/capture_mode_bar_view.h" #include "ash/capture_mode/capture_mode_button.h" #include "ash/capture_mode/capture_mode_constants.h" #include "ash/capture_mode/capture_mode_controller.h" #include "ash/capture_mode/capture_mode_menu_group.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_settings_view.h" #include "ash/capture_mode/capture_mode_source_view.h" #include "ash/capture_mode/capture_mode_test_util.h" #include "ash/capture_mode/capture_mode_toggle_button.h" #include "ash/capture_mode/capture_mode_type_view.h" #include "ash/capture_mode/capture_mode_types.h" #include "ash/capture_mode/capture_mode_util.h" #include "ash/capture_mode/fake_folder_selection_dialog_factory.h" #include "ash/capture_mode/recording_overlay_controller.h" #include "ash/capture_mode/stop_recording_button_tray.h" #include "ash/capture_mode/test_capture_mode_delegate.h" #include "ash/capture_mode/user_nudge_controller.h" #include "ash/capture_mode/video_recording_watcher.h" #include "ash/constants/ash_features.h" #include "ash/display/cursor_window_controller.h" #include "ash/display/output_protection_delegate.h" #include "ash/display/screen_orientation_controller_test_api.h" #include "ash/display/window_tree_host_manager.h" #include "ash/projector/projector_annotation_tray.h" #include "ash/projector/projector_controller_impl.h" #include "ash/projector/projector_metrics.h" #include "ash/public/cpp/capture_mode/capture_mode_test_api.h" #include "ash/public/cpp/holding_space/holding_space_test_api.h" #include "ash/public/cpp/projector/projector_new_screencast_precondition.h" #include "ash/public/cpp/projector/projector_session.h" #include "ash/public/cpp/shelf_types.h" #include "ash/public/cpp/shell_window_ids.h" #include "ash/public/cpp/test/mock_projector_client.h" #include "ash/public/cpp/test/shell_test_api.h" #include "ash/root_window_controller.h" #include "ash/services/recording/recording_service_test_api.h" #include "ash/shell.h" #include "ash/system/status_area_widget.h" #include "ash/test/ash_test_base.h" #include "ash/test/test_widget_builder.h" #include "ash/wm/desks/desk.h" #include "ash/wm/desks/desks_controller.h" #include "ash/wm/desks/desks_test_util.h" #include "ash/wm/overview/overview_controller.h" #include "ash/wm/tablet_mode/tablet_mode_controller.h" #include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h" #include "ash/wm/window_state.h" #include "ash/wm/window_util.h" #include "base/bind.h" #include "base/callback_forward.h" #include "base/callback_helpers.h" #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/memory/weak_ptr.h" #include "base/run_loop.h" #include "base/scoped_observation.h" #include "base/strings/stringprintf.h" #include "base/test/bind.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" #include "chromeos/dbus/power/fake_power_manager_client.h" #include "chromeos/dbus/power_manager/suspend.pb.h" #include "components/account_id/account_id.h" #include "components/user_manager/user_type.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 "services/viz/privileged/mojom/compositing/frame_sink_video_capture.mojom.h" #include "ui/aura/client/aura_constants.h" #include "ui/aura/client/capture_client.h" #include "ui/aura/client/capture_client_observer.h" #include "ui/aura/client/window_parenting_client.h" #include "ui/aura/window_tracker.h" #include "ui/base/clipboard/clipboard.h" #include "ui/base/clipboard/clipboard_buffer.h" #include "ui/base/cursor/cursor_factory.h" #include "ui/base/cursor/mojom/cursor_type.mojom-shared.h" #include "ui/compositor/compositor.h" #include "ui/compositor/layer.h" #include "ui/compositor/scoped_animation_duration_scale_mode.h" #include "ui/display/types/display_constants.h" #include "ui/display/util/display_util.h" #include "ui/events/event_constants.h" #include "ui/events/event_handler.h" #include "ui/events/test/event_generator.h" #include "ui/gfx/geometry/dip_util.h" #include "ui/gfx/geometry/insets.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/size_conversions.h" #include "ui/gfx/geometry/vector2d.h" #include "ui/gfx/image/image_unittest_util.h" #include "ui/gfx/native_widget_types.h" #include "ui/message_center/message_center.h" #include "ui/message_center/message_center_observer.h" #include "ui/message_center/public/cpp/notification.h" #include "ui/message_center/public/cpp/notification_delegate.h" #include "ui/ozone/public/ozone_platform.h" #include "ui/views/accessibility/view_accessibility.h" #include "ui/views/view.h" #include "ui/views/widget/widget.h" #include "ui/wm/core/coordinate_conversion.h" #include "ui/wm/core/window_modality_controller.h" #include "ui/wm/core/window_util.h" namespace ash { namespace { constexpr char kEndRecordingReasonInClamshellHistogramName[] = "Ash.CaptureModeController.EndRecordingReason.ClamshellMode"; constexpr char kScreenCaptureNotificationId[] = "capture_mode_notification"; // Returns true if the software-composited cursor is enabled. bool IsCursorCompositingEnabled() { return Shell::Get() ->window_tree_host_manager() ->cursor_window_controller() ->is_cursor_compositing_enabled(); } const message_center::Notification* GetPreviewNotification() { const message_center::NotificationList::Notifications notifications = message_center::MessageCenter::Get()->GetVisibleNotifications(); for (const auto* notification : notifications) { if (notification->id() == kScreenCaptureNotificationId) return notification; } return nullptr; } void ClickNotification(absl::optional button_index) { const message_center::Notification* notification = GetPreviewNotification(); DCHECK(notification); notification->delegate()->Click(button_index, absl::nullopt); } // Sets up a callback that will be triggered when a capture file (image or // video) is deleted as a result of a user action. The callback will verify the // successful deletion of the file, and will quit the given `loop`. void SetUpFileDeletionVerifier(base::RunLoop* loop) { DCHECK(loop); CaptureModeTestApi().SetOnCaptureFileDeletedCallback( base::BindLambdaForTesting( [loop](const base::FilePath& path, bool delete_successful) { EXPECT_TRUE(delete_successful); base::ScopedAllowBlockingForTesting allow_blocking; EXPECT_FALSE(base::PathExists(path)); loop->Quit(); })); } void LeaveTabletMode() { TabletModeControllerTestApi test_api; test_api.LeaveTabletMode(); } // Defines a capture client observer, that sets the input capture to the window // given to the constructor, and destroys it once capture is lost. class TestCaptureClientObserver : public aura::client::CaptureClientObserver { public: explicit TestCaptureClientObserver(std::unique_ptr window) : window_(std::move(window)) { DCHECK(window_); auto* capture_client = aura::client::GetCaptureClient(window_->GetRootWindow()); capture_client->SetCapture(window_.get()); capture_client->AddObserver(this); } ~TestCaptureClientObserver() override { StopObserving(); } // aura::client::CaptureClientObserver: void OnCaptureChanged(aura::Window* lost_capture, aura::Window* gained_capture) override { if (lost_capture != window_.get()) return; StopObserving(); window_.reset(); } private: void StopObserving() { if (!window_) return; auto* capture_client = aura::client::GetCaptureClient(window_->GetRootWindow()); capture_client->RemoveObserver(this); } std::unique_ptr window_; }; } // namespace class CaptureModeTest : public AshTestBase { public: CaptureModeTest() = default; explicit CaptureModeTest(base::test::TaskEnvironment::TimeSource time) : AshTestBase(time) {} CaptureModeTest(const CaptureModeTest&) = delete; CaptureModeTest& operator=(const CaptureModeTest&) = delete; ~CaptureModeTest() override = default; views::Widget* GetCaptureModeBarWidget() const { auto* session = CaptureModeController::Get()->capture_mode_session(); DCHECK(session); return session->capture_mode_bar_widget(); } views::Widget* GetCaptureModeLabelWidget() const { auto* session = CaptureModeController::Get()->capture_mode_session(); DCHECK(session); return CaptureModeSessionTestApi(session).GetCaptureLabelWidget(); } CaptureModeSettingsView* GetCaptureModeSettingsView() const { auto* session = CaptureModeController::Get()->capture_mode_session(); DCHECK(session); return CaptureModeSessionTestApi(session).GetCaptureModeSettingsView(); } views::Widget* GetCaptureModeSettingsWidget() const { auto* session = CaptureModeController::Get()->capture_mode_session(); DCHECK(session); return CaptureModeSessionTestApi(session).GetCaptureModeSettingsWidget(); } bool IsFolderSelectionDialogShown() const { auto* session = CaptureModeController::Get()->capture_mode_session(); DCHECK(session); return CaptureModeSessionTestApi(session).IsFolderSelectionDialogShown(); } bool IsAllCaptureSessionUisVisible() const { auto* session = CaptureModeController::Get()->capture_mode_session(); DCHECK(session); return CaptureModeSessionTestApi(session).IsAllUisVisible(); } CaptureModeToggleButton* GetImageToggleButton() const { auto* controller = CaptureModeController::Get(); DCHECK(controller->IsActive()); return GetCaptureModeBarView()->capture_type_view()->image_toggle_button(); } CaptureModeToggleButton* GetVideoToggleButton() const { auto* controller = CaptureModeController::Get(); DCHECK(controller->IsActive()); return GetCaptureModeBarView()->capture_type_view()->video_toggle_button(); } CaptureModeToggleButton* GetWindowToggleButton() const { auto* controller = CaptureModeController::Get(); DCHECK(controller->IsActive()); return GetCaptureModeBarView() ->capture_source_view() ->window_toggle_button(); } CaptureModeToggleButton* GetSettingsButton() const { auto* controller = CaptureModeController::Get(); DCHECK(controller->IsActive()); return GetCaptureModeBarView()->settings_button(); } CaptureModeButton* GetCloseButton() const { auto* controller = CaptureModeController::Get(); DCHECK(controller->IsActive()); return GetCaptureModeBarView()->close_button(); } aura::Window* GetDimensionsLabelWindow() const { auto* controller = CaptureModeController::Get(); DCHECK(controller->IsActive()); auto* widget = CaptureModeSessionTestApi(controller->capture_mode_session()) .GetDimensionsLabelWidget(); return widget ? widget->GetNativeWindow() : nullptr; } absl::optional GetMagnifierGlassCenterPoint() const { auto* controller = CaptureModeController::Get(); DCHECK(controller->IsActive()); auto& magnifier = CaptureModeSessionTestApi(controller->capture_mode_session()) .GetMagnifierGlass(); if (magnifier.host_widget_for_testing()) { return magnifier.host_widget_for_testing() ->GetWindowBoundsInScreen() .CenterPoint(); } return absl::nullopt; } // Start Capture Mode with source region and type image. CaptureModeController* StartImageRegionCapture() { return StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kImage); } CaptureModeController* StartSessionAndRecordWindow(aura::Window* window) { auto* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo); GetEventGenerator()->MoveMouseToCenterOf(window); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); return controller; } // Select a region by pressing and dragging the mouse. void SelectRegion(const gfx::Rect& region_in_screen, bool release_mouse = true) { auto* controller = CaptureModeController::Get(); ASSERT_TRUE(controller->IsActive()); ASSERT_EQ(CaptureModeSource::kRegion, controller->source()); auto* event_generator = GetEventGenerator(); event_generator->set_current_screen_location(region_in_screen.origin()); event_generator->PressLeftButton(); event_generator->MoveMouseTo(region_in_screen.bottom_right()); if (release_mouse) event_generator->ReleaseLeftButton(); auto capture_region_in_root = region_in_screen; wm::ConvertRectFromScreen( controller->capture_mode_session()->current_root(), &capture_region_in_root); EXPECT_EQ(capture_region_in_root, controller->user_capture_region()); } void WaitForSessionToEnd() { auto* controller = CaptureModeController::Get(); if (!controller->IsActive()) return; auto* test_delegate = static_cast( controller->delegate_for_testing()); ASSERT_TRUE(test_delegate); base::RunLoop run_loop; test_delegate->set_on_session_state_changed_callback( run_loop.QuitClosure()); run_loop.Run(); ASSERT_FALSE(controller->IsActive()); } void RemoveSecondaryDisplay() { const int64_t primary_id = WindowTreeHostManager::GetPrimaryDisplayId(); display::ManagedDisplayInfo primary_info = display_manager()->GetDisplayInfo(primary_id); std::vector display_info_list; display_info_list.push_back(primary_info); display_manager()->OnNativeDisplaysChanged(display_info_list); // Spin the run loop so that we get a signal that the associated root window // of the removed display is destroyed. base::RunLoop().RunUntilIdle(); } void SwitchToUser2() { auto* session_controller = GetSessionControllerClient(); constexpr char kUserEmail[] = "user2@capture_mode"; session_controller->AddUserSession(kUserEmail); session_controller->SwitchActiveUser(AccountId::FromUserEmail(kUserEmail)); } base::FilePath CreateFolderOnDriveFS(const std::string& custom_folder_name) { auto* test_delegate = CaptureModeController::Get()->delegate_for_testing(); base::FilePath mount_point_path; EXPECT_TRUE(test_delegate->GetDriveFsMountPointPath(&mount_point_path)); base::FilePath folder_on_drive_fs = mount_point_path.Append("root").Append(custom_folder_name); base::ScopedAllowBlockingForTesting allow_blocking; const bool result = base::CreateDirectory(folder_on_drive_fs); EXPECT_TRUE(result); return folder_on_drive_fs; } void OpenSettingsView() { CaptureModeSession* session = CaptureModeController::Get()->capture_mode_session(); DCHECK(session); ClickOnView(CaptureModeSessionTestApi(session) .GetCaptureModeBarView() ->settings_button(), GetEventGenerator()); } std::unique_ptr CreateTransientModalChildWindow( gfx::Rect child_window_bounds, aura::Window* transient_parent) { auto child = CreateTestWindow(child_window_bounds); wm::AddTransientChild(transient_parent, child.get()); child->Show(); child->SetProperty(aura::client::kModalKey, ui::MODAL_TYPE_WINDOW); wm::SetModalParent(child.get(), transient_parent); return child; } }; class CaptureSessionWidgetClosed { public: explicit CaptureSessionWidgetClosed(views::Widget* widget) { DCHECK(widget); widget_ = widget->GetWeakPtr(); } CaptureSessionWidgetClosed(const CaptureSessionWidgetClosed&) = delete; CaptureSessionWidgetClosed& operator=(const CaptureSessionWidgetClosed&) = delete; ~CaptureSessionWidgetClosed() = default; bool GetWidgetClosed() const { return !widget_ || widget_->IsClosed(); } private: base::WeakPtr widget_; }; class CaptureNotificationWaiter : public message_center::MessageCenterObserver { public: CaptureNotificationWaiter() { message_center::MessageCenter::Get()->AddObserver(this); } ~CaptureNotificationWaiter() override { message_center::MessageCenter::Get()->RemoveObserver(this); } void Wait() { run_loop_.Run(); } // message_center::MessageCenterObserver: void OnNotificationAdded(const std::string& notification_id) override { if (notification_id == kScreenCaptureNotificationId) run_loop_.Quit(); } private: base::RunLoop run_loop_; }; TEST_F(CaptureModeTest, StartStop) { auto* controller = CaptureModeController::Get(); controller->Start(CaptureModeEntryType::kQuickSettings); EXPECT_TRUE(controller->IsActive()); // Calling start again is a no-op. controller->Start(CaptureModeEntryType::kQuickSettings); EXPECT_TRUE(controller->IsActive()); // Closing the session should close the native window of capture mode bar // immediately. auto* bar_window = GetCaptureModeBarWidget()->GetNativeWindow(); aura::WindowTracker tracker({bar_window}); controller->Stop(); EXPECT_TRUE(tracker.windows().empty()); EXPECT_FALSE(controller->IsActive()); } TEST_F(CaptureModeTest, CheckCursorVisibility) { // Hide cursor before entering capture mode. auto* cursor_manager = Shell::Get()->cursor_manager(); cursor_manager->SetCursor(ui::mojom::CursorType::kPointer); cursor_manager->HideCursor(); cursor_manager->DisableMouseEvents(); EXPECT_FALSE(cursor_manager->IsCursorVisible()); auto* controller = CaptureModeController::Get(); controller->Start(CaptureModeEntryType::kQuickSettings); // After capture mode initialization, cursor should be visible. EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_TRUE(cursor_manager->IsMouseEventsEnabled()); // Enter tablet mode. SwitchToTabletMode(); // After entering tablet mode, cursor should be invisible and locked. EXPECT_FALSE(cursor_manager->IsCursorVisible()); EXPECT_TRUE(cursor_manager->IsCursorLocked()); // Leave tablet mode, cursor should be visible again. LeaveTabletMode(); EXPECT_TRUE(cursor_manager->IsCursorVisible()); } TEST_F(CaptureModeTest, CheckCursorVisibilityOnTabletMode) { auto* cursor_manager = Shell::Get()->cursor_manager(); // Enter tablet mode. SwitchToTabletMode(); // After entering tablet mode, cursor should be invisible. EXPECT_FALSE(cursor_manager->IsCursorVisible()); // Open capture mode. auto* controller = CaptureModeController::Get(); controller->Start(CaptureModeEntryType::kQuickSettings); // Cursor should be invisible since it's still in tablet mode. EXPECT_FALSE(cursor_manager->IsCursorVisible()); // Leave tablet mode, cursor should be visible now. LeaveTabletMode(); EXPECT_TRUE(cursor_manager->IsCursorVisible()); } // Regression test for https://crbug.com/1172425. TEST_F(CaptureModeTest, NoCrashOnClearingCapture) { TestCaptureClientObserver observer(CreateTestWindow(gfx::Rect(200, 200))); auto* controller = StartImageRegionCapture(); EXPECT_TRUE(controller->IsActive()); } TEST_F(CaptureModeTest, CheckWidgetClosed) { auto* controller = CaptureModeController::Get(); controller->Start(CaptureModeEntryType::kQuickSettings); EXPECT_TRUE(controller->IsActive()); EXPECT_TRUE(GetCaptureModeBarWidget()); CaptureSessionWidgetClosed observer(GetCaptureModeBarWidget()); EXPECT_FALSE(observer.GetWidgetClosed()); controller->Stop(); EXPECT_FALSE(controller->IsActive()); EXPECT_FALSE(controller->capture_mode_session()); // The Widget should have been destroyed by now. EXPECT_TRUE(observer.GetWidgetClosed()); } TEST_F(CaptureModeTest, StartWithMostRecentTypeAndSource) { auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kFullscreen); controller->SetType(CaptureModeType::kVideo); controller->Start(CaptureModeEntryType::kQuickSettings); EXPECT_TRUE(controller->IsActive()); EXPECT_FALSE(GetImageToggleButton()->GetToggled()); EXPECT_TRUE(GetVideoToggleButton()->GetToggled()); EXPECT_TRUE(GetFullscreenToggleButton()->GetToggled()); EXPECT_FALSE(GetRegionToggleButton()->GetToggled()); EXPECT_FALSE(GetWindowToggleButton()->GetToggled()); ClickOnView(GetCloseButton(), GetEventGenerator()); EXPECT_FALSE(controller->IsActive()); } TEST_F(CaptureModeTest, ChangeTypeAndSourceFromUI) { auto* controller = CaptureModeController::Get(); controller->Start(CaptureModeEntryType::kQuickSettings); EXPECT_TRUE(controller->IsActive()); EXPECT_TRUE(GetImageToggleButton()->GetToggled()); EXPECT_FALSE(GetVideoToggleButton()->GetToggled()); auto* event_generator = GetEventGenerator(); ClickOnView(GetVideoToggleButton(), event_generator); EXPECT_FALSE(GetImageToggleButton()->GetToggled()); EXPECT_TRUE(GetVideoToggleButton()->GetToggled()); EXPECT_EQ(controller->type(), CaptureModeType::kVideo); ClickOnView(GetWindowToggleButton(), event_generator); EXPECT_FALSE(GetFullscreenToggleButton()->GetToggled()); EXPECT_FALSE(GetRegionToggleButton()->GetToggled()); EXPECT_TRUE(GetWindowToggleButton()->GetToggled()); EXPECT_EQ(controller->source(), CaptureModeSource::kWindow); ClickOnView(GetFullscreenToggleButton(), event_generator); EXPECT_TRUE(GetFullscreenToggleButton()->GetToggled()); EXPECT_FALSE(GetRegionToggleButton()->GetToggled()); EXPECT_FALSE(GetWindowToggleButton()->GetToggled()); EXPECT_EQ(controller->source(), CaptureModeSource::kFullscreen); } TEST_F(CaptureModeTest, VideoRecordingUiBehavior) { // Start Capture Mode in a fullscreen video recording mode. CaptureModeController* controller = StartCaptureSession( CaptureModeSource::kFullscreen, CaptureModeType::kVideo); EXPECT_TRUE(controller->IsActive()); EXPECT_FALSE(controller->is_recording_in_progress()); EXPECT_FALSE(IsCursorCompositingEnabled()); // Hit Enter to begin recording. auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_RETURN, event_generator); EXPECT_EQ(ui::mojom::CursorType::kPointer, Shell::Get()->cursor_manager()->GetCursor().type()); WaitForRecordingToStart(); EXPECT_FALSE(controller->IsActive()); EXPECT_TRUE(controller->is_recording_in_progress()); // The composited cursor should remain disabled now that we're using the // cursor overlay on the capturer. The stop-recording button should show up in // the status area widget. EXPECT_FALSE(IsCursorCompositingEnabled()); auto* stop_recording_button = Shell::GetPrimaryRootWindowController() ->GetStatusAreaWidget() ->stop_recording_button_tray(); EXPECT_TRUE(stop_recording_button->visible_preferred()); // End recording via the stop-recording button. Expect that it's now hidden. base::HistogramTester histogram_tester; ClickOnView(stop_recording_button, event_generator); EXPECT_FALSE(stop_recording_button->visible_preferred()); EXPECT_FALSE(controller->is_recording_in_progress()); EXPECT_FALSE(IsCursorCompositingEnabled()); histogram_tester.ExpectBucketCount( kEndRecordingReasonInClamshellHistogramName, EndRecordingReason::kStopRecordingButton, 1); } // Tests the behavior of repositioning a region with capture mode. TEST_F(CaptureModeTest, CaptureRegionRepositionBehavior) { // Use a set display size as we will be choosing points in this test. UpdateDisplay("800x700"); auto* controller = StartImageRegionCapture(); // The first time selecting a region, the region is a default rect. EXPECT_EQ(gfx::Rect(), controller->user_capture_region()); // Press down and drag to select a region. SelectRegion(gfx::Rect(100, 100, 600, 600)); // Click somewhere in the center on the region and drag. The whole region // should move. Note that the point cannot be in the capture button bounds, // which is located in the center of the region. auto* event_generator = GetEventGenerator(); event_generator->set_current_screen_location(gfx::Point(200, 200)); event_generator->DragMouseBy(-50, -50); EXPECT_EQ(gfx::Rect(50, 50, 600, 600), controller->user_capture_region()); // Try to drag the region offscreen. The region should be bound by the display // size. event_generator->set_current_screen_location(gfx::Point(100, 100)); event_generator->DragMouseBy(-150, -150); EXPECT_EQ(gfx::Rect(600, 600), controller->user_capture_region()); } // Tests the behavior of resizing a region with capture mode using the corner // drag affordances. TEST_F(CaptureModeTest, CaptureRegionCornerResizeBehavior) { // Use a set display size as we will be choosing points in this test. UpdateDisplay("800x700"); auto* controller = StartImageRegionCapture(); // Create the initial region. const gfx::Rect target_region(gfx::Rect(200, 200, 400, 400)); SelectRegion(target_region); // For each corner point try dragging to several points and verify that the // capture region is as expected. struct { std::string trace; gfx::Point drag_point; // The point that stays the same while dragging. It is the opposite vertex // to |drag_point| on |target_region|. gfx::Point anchor_point; } kDragCornerCases[] = { {"origin", target_region.origin(), target_region.bottom_right()}, {"top_right", target_region.top_right(), target_region.bottom_left()}, {"bottom_right", target_region.bottom_right(), target_region.origin()}, {"bottom_left", target_region.bottom_left(), target_region.top_right()}, }; // The test corner points are one in each corner outside |target_region| and // one point inside |target_region|. auto drag_test_points = {gfx::Point(100, 100), gfx::Point(700, 100), gfx::Point(700, 700), gfx::Point(100, 700), gfx::Point(400, 400)}; auto* event_generator = GetEventGenerator(); for (auto test_case : kDragCornerCases) { SCOPED_TRACE(test_case.trace); event_generator->set_current_screen_location(test_case.drag_point); event_generator->PressLeftButton(); // At each drag test point, the region rect should be the rect created by // the given |corner_point| and the drag test point. That is, the width // should match the x distance between the two points, the height should // match the y distance between the two points and that both points are // contained in the region. for (auto drag_test_point : drag_test_points) { event_generator->MoveMouseTo(drag_test_point); gfx::Rect region = controller->user_capture_region(); const gfx::Vector2d distance = test_case.anchor_point - drag_test_point; EXPECT_EQ(std::abs(distance.x()), region.width()); EXPECT_EQ(std::abs(distance.y()), region.height()); // gfx::Rect::Contains returns the point (x+width, y+height) as false, so // make the region one unit bigger to account for this. region.Inset(gfx::Insets(-1)); EXPECT_TRUE(region.Contains(drag_test_point)); EXPECT_TRUE(region.Contains(test_case.anchor_point)); } // Make sure the region is reset for the next iteration. event_generator->MoveMouseTo(test_case.drag_point); event_generator->ReleaseLeftButton(); ASSERT_EQ(target_region, controller->user_capture_region()); } } // Tests the behavior of resizing a region with capture mode using the edge drag // affordances. TEST_F(CaptureModeTest, CaptureRegionEdgeResizeBehavior) { // Use a set display size as we will be choosing points in this test. UpdateDisplay("800x700"); auto* controller = StartImageRegionCapture(); // Create the initial region. const gfx::Rect target_region(gfx::Rect(200, 200, 200, 200)); SelectRegion(target_region); // For each edge point try dragging to several points and verify that the // capture region is as expected. struct { std::string trace; gfx::Point drag_point; // True if horizontal direction (left, right). Height stays the same while // dragging if true, width stays the same while dragging if false. bool horizontal; // The edge that stays the same while dragging. It is the opposite edge to // |drag_point|. For example, if |drag_point| is the left center of // |target_region|, then |anchor_edge| is the right edge. int anchor_edge; } kDragEdgeCases[] = { {"left", target_region.left_center(), true, target_region.right()}, {"top", target_region.top_center(), false, target_region.bottom()}, {"right", target_region.right_center(), true, target_region.x()}, {"bottom", target_region.bottom_center(), false, target_region.y()}, }; // Drag to a couple of points that change both x and y. In all these cases, // only the width or height should change. auto drag_test_points = {gfx::Point(150, 150), gfx::Point(350, 350), gfx::Point(450, 450)}; auto* event_generator = GetEventGenerator(); for (auto test_case : kDragEdgeCases) { SCOPED_TRACE(test_case.trace); event_generator->set_current_screen_location(test_case.drag_point); event_generator->PressLeftButton(); for (auto drag_test_point : drag_test_points) { event_generator->MoveMouseTo(drag_test_point); const gfx::Rect region = controller->user_capture_region(); // One of width/height will always be the same as |target_region|'s // initial width/height, depending on the edge affordance. The other // dimension will be the distance from |drag_test_point| to the anchor // edge. const int variable_length = std::abs( (test_case.horizontal ? drag_test_point.x() : drag_test_point.y()) - test_case.anchor_edge); const int expected_width = test_case.horizontal ? variable_length : target_region.width(); const int expected_height = test_case.horizontal ? target_region.height() : variable_length; EXPECT_EQ(expected_width, region.width()); EXPECT_EQ(expected_height, region.height()); } // Make sure the region is reset for the next iteration. event_generator->MoveMouseTo(test_case.drag_point); event_generator->ReleaseLeftButton(); ASSERT_EQ(target_region, controller->user_capture_region()); } } // Tests that the capture region persists after exiting and reentering capture // mode. TEST_F(CaptureModeTest, CaptureRegionPersistsAfterExit) { auto* controller = StartImageRegionCapture(); const gfx::Rect region(100, 100, 200, 200); SelectRegion(region); controller->Stop(); controller->Start(CaptureModeEntryType::kQuickSettings); EXPECT_EQ(region, controller->user_capture_region()); } // Tests that the capture region resets when clicking outside the current // capture regions bounds. TEST_F(CaptureModeTest, CaptureRegionResetsOnClickOutside) { auto* controller = StartImageRegionCapture(); SelectRegion(gfx::Rect(100, 100, 200, 200)); // Click on an area outside of the current capture region. The capture region // should reset to default rect. auto* event_generator = GetEventGenerator(); event_generator->set_current_screen_location(gfx::Point(400, 400)); event_generator->ClickLeftButton(); EXPECT_EQ(gfx::Rect(), controller->user_capture_region()); } // Tests that buttons on the capture mode bar still work when a region is // "covering" them. TEST_F(CaptureModeTest, CaptureRegionCoversCaptureModeBar) { UpdateDisplay("800x700"); auto* controller = StartImageRegionCapture(); // Select a region such that the capture mode bar is covered. SelectRegion(gfx::Rect(5, 5, 795, 795)); EXPECT_TRUE(controller->user_capture_region().Contains( GetCaptureModeBarView()->GetBoundsInScreen())); // Click on the fullscreen toggle button to verify that we enter fullscreen // capture mode. Then click on the region toggle button to verify that we // reenter region capture mode and that the region is still covering the // capture mode bar. auto* event_generator = GetEventGenerator(); ClickOnView(GetFullscreenToggleButton(), event_generator); EXPECT_EQ(CaptureModeSource::kFullscreen, controller->source()); ClickOnView(GetRegionToggleButton(), GetEventGenerator()); ASSERT_EQ(CaptureModeSource::kRegion, controller->source()); ASSERT_TRUE(controller->user_capture_region().Contains( GetCaptureModeBarView()->GetBoundsInScreen())); ClickOnView(GetCloseButton(), event_generator); EXPECT_FALSE(controller->IsActive()); } // Tests that the magnifying glass appears while fine tuning the capture region, // and that the cursor is hidden if the magnifying glass is present. TEST_F(CaptureModeTest, CaptureRegionMagnifierWhenFineTuning) { const gfx::Vector2d kDragDelta(50, 50); UpdateDisplay("800x700"); // Start Capture Mode in a region in image mode. StartImageRegionCapture(); // Press down and drag to select a region. The magnifier should not be // visible yet. gfx::Rect capture_region{200, 200, 400, 400}; SelectRegion(capture_region); EXPECT_EQ(absl::nullopt, GetMagnifierGlassCenterPoint()); auto check_magnifier_shows_properly = [this](const gfx::Point& origin, const gfx::Point& destination, bool should_show_magnifier) { // If |should_show_magnifier|, check that the magnifying glass is centered // on the mouse after press and during drag, and that the cursor is hidden. // If not |should_show_magnifier|, check that the magnifying glass never // shows. Should always be not visible when mouse button is released. auto* event_generator = GetEventGenerator(); absl::optional expected_origin = should_show_magnifier ? absl::make_optional(origin) : absl::nullopt; absl::optional expected_destination = should_show_magnifier ? absl::make_optional(destination) : absl::nullopt; auto* cursor_manager = Shell::Get()->cursor_manager(); EXPECT_TRUE(cursor_manager->IsCursorVisible()); // Move cursor to |origin| and click. event_generator->set_current_screen_location(origin); event_generator->PressLeftButton(); EXPECT_EQ(expected_origin, GetMagnifierGlassCenterPoint()); EXPECT_NE(should_show_magnifier, cursor_manager->IsCursorVisible()); // Drag to |destination| while holding left button. event_generator->MoveMouseTo(destination); EXPECT_EQ(expected_destination, GetMagnifierGlassCenterPoint()); EXPECT_NE(should_show_magnifier, cursor_manager->IsCursorVisible()); // Drag back to |origin| while still holding left button. event_generator->MoveMouseTo(origin); EXPECT_EQ(expected_origin, GetMagnifierGlassCenterPoint()); EXPECT_NE(should_show_magnifier, cursor_manager->IsCursorVisible()); // Release left button. event_generator->ReleaseLeftButton(); EXPECT_EQ(absl::nullopt, GetMagnifierGlassCenterPoint()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); }; // Drag the capture region from within the existing selected region. The // magnifier should not be visible at any point. check_magnifier_shows_properly(gfx::Point(400, 250), gfx::Point(500, 350), /*should_show_magnifier=*/false); // Check that each corner fine tune position shows the magnifier when // dragging. struct { std::string trace; FineTunePosition position; } kFineTunePositions[] = {{"top_left", FineTunePosition::kTopLeft}, {"top_right", FineTunePosition::kTopRight}, {"bottom_right", FineTunePosition::kBottomRight}, {"bottom_left", FineTunePosition::kBottomLeft}}; for (const auto& fine_tune_position : kFineTunePositions) { SCOPED_TRACE(fine_tune_position.trace); const gfx::Point drag_affordance_location = capture_mode_util::GetLocationForFineTunePosition( capture_region, fine_tune_position.position); check_magnifier_shows_properly(drag_affordance_location, drag_affordance_location + kDragDelta, /*should_show_magnifier=*/true); } } // Tests that the dimensions label properly renders for capture regions. TEST_F(CaptureModeTest, CaptureRegionDimensionsLabelLocation) { UpdateDisplay("900x800"); // Start Capture Mode in a region in image mode. StartImageRegionCapture(); // Press down and don't move the mouse. Label shouldn't display for empty // capture regions. auto* generator = GetEventGenerator(); generator->set_current_screen_location(gfx::Point(0, 0)); generator->PressLeftButton(); auto* controller = CaptureModeController::Get(); EXPECT_TRUE(controller->IsActive()); EXPECT_TRUE(controller->user_capture_region().IsEmpty()); EXPECT_EQ(nullptr, GetDimensionsLabelWindow()); generator->ReleaseLeftButton(); // Press down and drag to select a large region. Verify that the dimensions // label is centered and that the label is below the capture region. gfx::Rect capture_region{100, 100, 600, 200}; SelectRegion(capture_region, /*release_mouse=*/false); EXPECT_EQ(capture_region.CenterPoint().x(), GetDimensionsLabelWindow()->bounds().CenterPoint().x()); EXPECT_EQ(capture_region.bottom() + CaptureModeSession::kSizeLabelYDistanceFromRegionDp, GetDimensionsLabelWindow()->bounds().y()); generator->ReleaseLeftButton(); EXPECT_EQ(nullptr, GetDimensionsLabelWindow()); // Create a new capture region close to the left side of the screen such that // if the label was centered it would extend out of the screen. // The x value of the label should be the left edge of the screen (0). capture_region.SetRect(2, 100, 2, 100); SelectRegion(capture_region, /*release_mouse=*/false); EXPECT_EQ(0, GetDimensionsLabelWindow()->bounds().x()); generator->ReleaseLeftButton(); EXPECT_EQ(nullptr, GetDimensionsLabelWindow()); // Create a new capture region close to the right side of the screen such that // if the label was centered it would extend out of the screen. // The right (x + width) of the label should be the right edge of the screen // (900). capture_region.SetRect(896, 100, 2, 100); SelectRegion(capture_region, /*release_mouse=*/false); EXPECT_EQ(900, GetDimensionsLabelWindow()->bounds().right()); generator->ReleaseLeftButton(); EXPECT_EQ(nullptr, GetDimensionsLabelWindow()); // Create a new capture region close to the bottom side of the screen. // The label should now appear inside the capture region, just above the // bottom edge. It should be above the bottom of the screen as well. capture_region.SetRect(100, 700, 600, 790); SelectRegion(capture_region, /*release_mouse=*/false); EXPECT_EQ(800 - CaptureModeSession::kSizeLabelYDistanceFromRegionDp, GetDimensionsLabelWindow()->bounds().bottom()); generator->ReleaseLeftButton(); EXPECT_EQ(nullptr, GetDimensionsLabelWindow()); } TEST_F(CaptureModeTest, CaptureRegionCaptureButtonLocation) { UpdateDisplay("900x800"); auto* controller = StartImageRegionCapture(); // Select a large region. Verify that the capture button widget is centered. SelectRegion(gfx::Rect(100, 100, 600, 600)); views::Widget* capture_button_widget = GetCaptureModeLabelWidget(); ASSERT_TRUE(capture_button_widget); aura::Window* capture_button_window = capture_button_widget->GetNativeWindow(); EXPECT_EQ(gfx::Point(400, 400), capture_button_window->bounds().CenterPoint()); // Drag the bottom corner so that the region is too small to fit the capture // button. Verify that the button is aligned horizontally and placed below the // region. auto* event_generator = GetEventGenerator(); event_generator->DragMouseTo(gfx::Point(120, 120)); EXPECT_EQ(gfx::Rect(100, 100, 20, 20), controller->user_capture_region()); EXPECT_EQ(110, capture_button_window->bounds().CenterPoint().x()); const int distance_from_region = CaptureModeSession::kCaptureButtonDistanceFromRegionDp; EXPECT_EQ(120 + distance_from_region, capture_button_window->bounds().y()); // Click inside the region to drag the entire region to the bottom of the // screen. Verify that the button is aligned horizontally and placed above the // region. event_generator->set_current_screen_location(gfx::Point(110, 110)); event_generator->DragMouseTo(gfx::Point(110, 790)); EXPECT_EQ(gfx::Rect(100, 780, 20, 20), controller->user_capture_region()); EXPECT_EQ(110, capture_button_window->bounds().CenterPoint().x()); EXPECT_EQ(780 - distance_from_region, capture_button_window->bounds().bottom()); } // Tests some edge cases to ensure the capture button does not intersect the // capture bar and end up unclickable since it is stacked below the capture bar. // Regression test for https://crbug.com/1186462. TEST_F(CaptureModeTest, CaptureRegionCaptureButtonDoesNotIntersectCaptureBar) { UpdateDisplay("800x700"); StartImageRegionCapture(); // Create a region that would cover the capture mode bar. Add some insets to // ensure that the capture button could fit inside. Verify that the two // widgets do not overlap. const gfx::Rect capture_bar_bounds = GetCaptureModeBarWidget()->GetWindowBoundsInScreen(); gfx::Rect region_bounds = capture_bar_bounds; region_bounds.Inset(-20); SelectRegion(region_bounds); EXPECT_FALSE(capture_bar_bounds.Intersects( GetCaptureModeLabelWidget()->GetWindowBoundsInScreen())); // Create a thin region above the capture mode bar. The algorithm would // normally place the capture label under the region, but should adjust to // avoid intersecting. auto* event_generator = GetEventGenerator(); event_generator->set_current_screen_location(gfx::Point()); event_generator->ClickLeftButton(); const int capture_bar_midpoint_x = capture_bar_bounds.CenterPoint().x(); SelectRegion( gfx::Rect(capture_bar_midpoint_x, capture_bar_bounds.y() - 10, 20, 10)); EXPECT_FALSE(capture_bar_bounds.Intersects( GetCaptureModeLabelWidget()->GetWindowBoundsInScreen())); // Create a thin region below the capture mode bar which reaches the bottom of // the display. The algorithm would normally place the capture label above // the region, but should adjust to avoid intersecting. event_generator->set_current_screen_location(gfx::Point()); event_generator->ClickLeftButton(); SelectRegion(gfx::Rect(capture_bar_midpoint_x, capture_bar_bounds.bottom(), 20, 800 - capture_bar_bounds.bottom())); EXPECT_FALSE(capture_bar_bounds.Intersects( GetCaptureModeLabelWidget()->GetWindowBoundsInScreen())); // Create a thin region that is vertical as tall as the display, and at the // left edge of the display. The capture label button should be right of the // region. event_generator->set_current_screen_location(gfx::Point()); event_generator->ClickLeftButton(); SelectRegion(gfx::Rect(20, 800)); EXPECT_GT(GetCaptureModeLabelWidget()->GetWindowBoundsInScreen().x(), 20); } // Tests that pressing on the capture bar and releasing the press outside of the // capture bar, the capture region could still be draggable and set. Regression // test for https://crbug.com/1325028. TEST_F(CaptureModeTest, SetCaptureRegionAfterPressOnCaptureBar) { UpdateDisplay("800x600"); auto* controller = StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo); auto* settings_button = GetSettingsButton(); // Press on the settings button without release. auto* event_generator = GetEventGenerator(); event_generator->MoveMouseTo( settings_button->GetBoundsInScreen().CenterPoint()); event_generator->PressLeftButton(); // Move mouse to the outside of the capture bar and then release the press. event_generator->MoveMouseTo({300, 300}); event_generator->ReleaseLeftButton(); // Set the capture region, and verify it's set successfully. const gfx::Rect region_bounds(100, 100, 200, 200); SelectRegion(region_bounds); EXPECT_EQ(controller->user_capture_region(), region_bounds); } TEST_F(CaptureModeTest, WindowCapture) { // Create 2 windows that overlap with each other. const gfx::Rect bounds1(0, 0, 200, 200); std::unique_ptr window1(CreateTestWindow(bounds1)); const gfx::Rect bounds2(150, 150, 200, 200); std::unique_ptr window2(CreateTestWindow(bounds2)); auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kWindow); controller->SetType(CaptureModeType::kImage); controller->Start(CaptureModeEntryType::kAccelTakeWindowScreenshot); EXPECT_TRUE(controller->IsActive()); auto* event_generator = GetEventGenerator(); event_generator->MoveMouseToCenterOf(window1.get()); auto* capture_mode_session = controller->capture_mode_session(); EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window1.get()); event_generator->MoveMouseToCenterOf(window2.get()); EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window2.get()); // Now move the mouse to the overlapped area. event_generator->MoveMouseTo(gfx::Point(175, 175)); EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window2.get()); // Close the current selected window should automatically focus to next one. window2.reset(); EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window1.get()); // Open another one on top also change the selected window. std::unique_ptr window3(CreateTestWindow(bounds2)); EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window3.get()); // Minimize the window should also automatically change the selected window. WindowState::Get(window3.get())->Minimize(); EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window1.get()); // Stop the capture session to avoid CaptureModeSession from receiving more // events during test tearing down. controller->Stop(); } // Tests that the capture bar is located on the root with the cursor when // starting capture mode. TEST_F(CaptureModeTest, MultiDisplayCaptureBarInitialLocation) { UpdateDisplay("800x700,801+0-800x700"); auto* event_generator = GetEventGenerator(); MoveMouseToAndUpdateCursorDisplay(gfx::Point(1000, 500), event_generator); auto* controller = StartImageRegionCapture(); EXPECT_TRUE(gfx::Rect(801, 0, 800, 800) .Contains(GetCaptureModeBarView()->GetBoundsInScreen())); controller->Stop(); MoveMouseToAndUpdateCursorDisplay(gfx::Point(100, 500), event_generator); StartImageRegionCapture(); EXPECT_TRUE(gfx::Rect(800, 800).Contains( GetCaptureModeBarView()->GetBoundsInScreen())); } // Tests behavior of a capture mode session if the active display is removed. TEST_F(CaptureModeTest, DisplayRemoval) { UpdateDisplay("800x700,801+0-800x700"); // Start capture mode on the secondary display. MoveMouseToAndUpdateCursorDisplay(gfx::Point(1000, 500), GetEventGenerator()); auto* controller = StartImageRegionCapture(); auto* session = controller->capture_mode_session(); EXPECT_TRUE(gfx::Rect(801, 0, 800, 800) .Contains(GetCaptureModeBarView()->GetBoundsInScreen())); ASSERT_EQ(Shell::GetAllRootWindows()[1], session->current_root()); RemoveSecondaryDisplay(); // Tests that the capture mode bar is now on the primary display. EXPECT_TRUE(gfx::Rect(800, 800).Contains( GetCaptureModeBarView()->GetBoundsInScreen())); ASSERT_EQ(Shell::GetAllRootWindows()[0], session->current_root()); } // Tests behavior of a capture mode session if the active display is removed // and countdown running. TEST_F(CaptureModeTest, DisplayRemovalWithCountdownVisible) { UpdateDisplay("800x700,801+0-800x700"); // Start capture mode on the secondary display. auto recorded_window = CreateTestWindow(gfx::Rect(1000, 200, 400, 400)); auto* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo); GetEventGenerator()->MoveMouseToCenterOf(recorded_window.get()); auto* session = controller->capture_mode_session(); RemoveSecondaryDisplay(); ASSERT_EQ(Shell::GetAllRootWindows()[0], session->current_root()); // Test passes if no crash. } // Tests behavior of a capture mode session if the active display is removed, // countdown running, fullscreen window, and in overview mode. TEST_F(CaptureModeTest, DisplayRemovalWithCountdownVisibleFullscreenWindowAndInOverview) { UpdateDisplay("800x700,801+0-800x700"); // Start capture mode on the secondary display. auto recorded_window = CreateTestWindow(gfx::Rect(1000, 200, 400, 400)); auto* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo); GetEventGenerator()->MoveMouseToCenterOf(recorded_window.get()); // Make the window fullscreen. This is important as the corner case is // moving a fullscreen window triggers the shelf to occur, which changes // display metrics. recorded_window->SetProperty(aura::client::kShowStateKey, ui::SHOW_STATE_FULLSCREEN); auto* session = controller->capture_mode_session(); EnterOverview(); RemoveSecondaryDisplay(); ASSERT_EQ(Shell::GetAllRootWindows()[0], session->current_root()); // Test passes if no crash. } // Tests that using fullscreen or window source, moving the mouse across // displays will change the root window of the capture session. TEST_F(CaptureModeTest, MultiDisplayFullscreenOrWindowSourceRootWindow) { UpdateDisplay("800x700,801+0-800x700"); ASSERT_EQ(2u, Shell::GetAllRootWindows().size()); auto* event_generator = GetEventGenerator(); MoveMouseToAndUpdateCursorDisplay(gfx::Point(100, 500), event_generator); for (auto source : {CaptureModeSource::kFullscreen, CaptureModeSource::kWindow}) { SCOPED_TRACE(source == CaptureModeSource::kFullscreen ? "Fullscreen source" : "Window source"); auto* controller = StartCaptureSession(source, CaptureModeType::kImage); auto* session = controller->capture_mode_session(); EXPECT_EQ(Shell::GetAllRootWindows()[0], session->current_root()); MoveMouseToAndUpdateCursorDisplay(gfx::Point(1000, 500), event_generator); EXPECT_EQ(Shell::GetAllRootWindows()[1], session->current_root()); MoveMouseToAndUpdateCursorDisplay(gfx::Point(100, 500), event_generator); EXPECT_EQ(Shell::GetAllRootWindows()[0], session->current_root()); controller->Stop(); } } // Tests that in region mode, moving the mouse across displays will not change // the root window of the capture session, but clicking on a new display will. TEST_F(CaptureModeTest, MultiDisplayRegionSourceRootWindow) { UpdateDisplay("800x700,801+0-800x700"); ASSERT_EQ(2u, Shell::GetAllRootWindows().size()); auto* event_generator = GetEventGenerator(); MoveMouseToAndUpdateCursorDisplay(gfx::Point(100, 500), event_generator); auto* controller = StartImageRegionCapture(); auto* session = controller->capture_mode_session(); EXPECT_EQ(Shell::GetAllRootWindows()[0], session->current_root()); // Tests that moving the mouse to the secondary display does not change the // root. MoveMouseToAndUpdateCursorDisplay(gfx::Point(1000, 500), event_generator); EXPECT_EQ(Shell::GetAllRootWindows()[0], session->current_root()); // Tests that pressing the mouse changes the root. The capture bar stays on // the primary display until the mouse is released. event_generator->PressLeftButton(); EXPECT_EQ(Shell::GetAllRootWindows()[1], session->current_root()); EXPECT_TRUE(gfx::Rect(800, 800).Contains( GetCaptureModeBarView()->GetBoundsInScreen())); event_generator->ReleaseLeftButton(); EXPECT_EQ(Shell::GetAllRootWindows()[1], session->current_root()); EXPECT_TRUE(gfx::Rect(801, 0, 800, 800) .Contains(GetCaptureModeBarView()->GetBoundsInScreen())); } // Tests that using touch on multi display setups works as intended. Regression // test for https://crbug.com/1159512. TEST_F(CaptureModeTest, MultiDisplayTouch) { UpdateDisplay("800x700,801+0-800x700"); ASSERT_EQ(2u, Shell::GetAllRootWindows().size()); auto* controller = StartImageRegionCapture(); auto* session = controller->capture_mode_session(); ASSERT_EQ(Shell::GetAllRootWindows()[0], session->current_root()); // Touch and move your finger on the secondary display. We should switch roots // and the region size should be as expected. auto* event_generator = GetEventGenerator(); event_generator->PressTouch(gfx::Point(1000, 200)); event_generator->MoveTouch(gfx::Point(1200, 400)); event_generator->ReleaseTouch(); EXPECT_EQ(Shell::GetAllRootWindows()[1], session->current_root()); EXPECT_EQ(gfx::Size(200, 200), controller->user_capture_region().size()); } TEST_F(CaptureModeTest, RegionCursorStates) { UpdateDisplay("800x700,801+0-800x700"); using ui::mojom::CursorType; auto* cursor_manager = Shell::Get()->cursor_manager(); auto* event_generator = GetEventGenerator(); struct { std::string scoped_trace; gfx::Rect display_rect; gfx::Point point; gfx::Rect capture_region; } kRegionTestCases[] = { {"primary_display", gfx::Rect(0, 0, 800, 700), gfx::Point(250, 250), gfx::Rect(200, 200, 200, 200)}, {"external_display", gfx::Rect(801, 0, 800, 700), gfx::Point(1050, 250), gfx::Rect(1000, 200, 200, 200)}, }; for (auto test_case : kRegionTestCases) { SCOPED_TRACE(test_case.scoped_trace); MoveMouseToAndUpdateCursorDisplay(test_case.point, event_generator); const CursorType original_cursor_type = cursor_manager->GetCursor().type(); EXPECT_FALSE(cursor_manager->IsCursorLocked()); auto* controller = StartImageRegionCapture(); EXPECT_TRUE(test_case.display_rect.Contains( GetCaptureModeBarView()->GetBoundsInScreen())); auto outside_point = test_case.capture_region.origin(); outside_point.Offset(-10, -10); // Clear the previous region if any. event_generator->MoveMouseTo(outside_point); event_generator->ClickLeftButton(); EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_EQ(CursorType::kCell, cursor_manager->GetCursor().type()); EXPECT_TRUE(cursor_manager->IsCursorLocked()); // Makes sure that the cursor is updated when the user releases the region // select and is still hovering in the same location. SelectRegion(test_case.capture_region); EXPECT_EQ(CursorType::kSouthEastResize, cursor_manager->GetCursor().type()); // Verify that all of the `FineTunePosition` locations have the correct // cursor when hovered over both in primary display and external display. event_generator->MoveMouseTo(test_case.capture_region.origin()); EXPECT_EQ(CursorType::kNorthWestResize, cursor_manager->GetCursor().type()); event_generator->MoveMouseTo(test_case.capture_region.top_center()); EXPECT_EQ(CursorType::kNorthSouthResize, cursor_manager->GetCursor().type()); event_generator->MoveMouseTo(test_case.capture_region.top_right()); EXPECT_EQ(CursorType::kNorthEastResize, cursor_manager->GetCursor().type()); event_generator->MoveMouseTo(test_case.capture_region.right_center()); EXPECT_EQ(CursorType::kEastWestResize, cursor_manager->GetCursor().type()); event_generator->MoveMouseTo(test_case.capture_region.bottom_right()); EXPECT_EQ(CursorType::kSouthEastResize, cursor_manager->GetCursor().type()); event_generator->MoveMouseTo(test_case.capture_region.bottom_center()); EXPECT_EQ(CursorType::kNorthSouthResize, cursor_manager->GetCursor().type()); event_generator->MoveMouseTo(test_case.capture_region.bottom_left()); EXPECT_EQ(CursorType::kSouthWestResize, cursor_manager->GetCursor().type()); event_generator->MoveMouseTo(test_case.capture_region.left_center()); EXPECT_EQ(CursorType::kEastWestResize, cursor_manager->GetCursor().type()); // Tests that within the bounds of the selected region, the cursor is a hand // when hovering over the capture button, otherwise it is a // multi-directional move cursor. event_generator->MoveMouseTo(test_case.point); EXPECT_EQ(CursorType::kMove, cursor_manager->GetCursor().type()); event_generator->MoveMouseTo(test_case.capture_region.CenterPoint()); EXPECT_EQ(CursorType::kHand, cursor_manager->GetCursor().type()); // Tests that the cursor changes to a cell type when hovering over the // unselected region. event_generator->MoveMouseTo(outside_point); EXPECT_EQ(CursorType::kCell, cursor_manager->GetCursor().type()); // Check that cursor is unlocked when changing sources, and that the cursor // changes to a pointer when hovering over the capture mode bar. event_generator->MoveMouseTo( GetRegionToggleButton()->GetBoundsInScreen().CenterPoint()); EXPECT_EQ(CursorType::kPointer, cursor_manager->GetCursor().type()); event_generator->MoveMouseTo( GetWindowToggleButton()->GetBoundsInScreen().CenterPoint()); EXPECT_EQ(CursorType::kPointer, cursor_manager->GetCursor().type()); event_generator->ClickLeftButton(); ASSERT_EQ(CaptureModeSource::kWindow, controller->source()); // The event on the capture bar to change capture source will still keep the // cursor locked. EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_EQ(CursorType::kPointer, cursor_manager->GetCursor().type()); // Tests that on changing back to region capture mode, the cursor becomes // locked, and is still a pointer type over the bar, whilst a cell cursor // otherwise (not over the selected region). event_generator->MoveMouseTo( GetRegionToggleButton()->GetBoundsInScreen().CenterPoint()); event_generator->ClickLeftButton(); EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_EQ(CursorType::kPointer, cursor_manager->GetCursor().type()); // Tests that clicking on the button again doesn't change the cursor. event_generator->ClickLeftButton(); EXPECT_EQ(CursorType::kPointer, cursor_manager->GetCursor().type()); event_generator->MoveMouseTo(outside_point); EXPECT_EQ(CursorType::kCell, cursor_manager->GetCursor().type()); // Tests that when exiting capture mode that the cursor is restored to its // original state. controller->Stop(); EXPECT_FALSE(controller->IsActive()); EXPECT_FALSE(cursor_manager->IsCursorLocked()); EXPECT_EQ(original_cursor_type, cursor_manager->GetCursor().type()); } // Tests the cursor state in tablet mode. auto* controller = StartImageRegionCapture(); // Enter tablet mode, the cursor should be hidden. SwitchToTabletMode(); EXPECT_FALSE(cursor_manager->IsCursorVisible()); EXPECT_TRUE(cursor_manager->IsCursorLocked()); // Move mouse but it should still be invisible. event_generator->MoveMouseTo(gfx::Point(100, 100)); EXPECT_FALSE(cursor_manager->IsCursorVisible()); EXPECT_TRUE(cursor_manager->IsCursorLocked()); // Return to clamshell mode, mouse should appear again. LeaveTabletMode(); EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_EQ(CursorType::kCell, cursor_manager->GetCursor().type()); controller->Stop(); } // Regression testing for https://crbug.com/1334824. TEST_F(CaptureModeTest, CursorShouldNotChangeWhileAdjustingRegion) { UpdateDisplay("800x600"); using ui::mojom::CursorType; auto* cursor_manager = Shell::Get()->cursor_manager(); auto* event_generator = GetEventGenerator(); EXPECT_FALSE(cursor_manager->IsCursorLocked()); StartImageRegionCapture(); event_generator->MoveMouseTo(gfx::Point(200, 200)); EXPECT_EQ(CursorType::kCell, cursor_manager->GetCursor().type()); event_generator->PressLeftButton(); event_generator->MoveMouseTo(gfx::Point(300, 300)); EXPECT_EQ(CursorType::kSouthEastResize, cursor_manager->GetCursor().type()); // Drag the region by moving the cursor to the center point of the capture bar // and expect that it doesn't change back to a pointer. const auto capture_bar_center = GetCaptureModeBarView()->GetBoundsInScreen().CenterPoint(); event_generator->MoveMouseTo(capture_bar_center); EXPECT_EQ(CursorType::kSouthEastResize, cursor_manager->GetCursor().type()); } TEST_F(CaptureModeTest, FullscreenCursorStates) { using ui::mojom::CursorType; auto* cursor_manager = Shell::Get()->cursor_manager(); CursorType original_cursor_type = cursor_manager->GetCursor().type(); EXPECT_FALSE(cursor_manager->IsCursorLocked()); EXPECT_EQ(CursorType::kPointer, original_cursor_type); auto* event_generator = GetEventGenerator(); CaptureModeController* controller = StartCaptureSession( CaptureModeSource::kFullscreen, CaptureModeType::kImage); EXPECT_EQ(controller->type(), CaptureModeType::kImage); EXPECT_TRUE(cursor_manager->IsCursorLocked()); event_generator->MoveMouseTo(gfx::Point(175, 175)); EXPECT_TRUE(cursor_manager->IsCursorVisible()); // Use image capture icon as the mouse cursor icon in image capture mode. EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); CaptureModeSessionTestApi test_api(controller->capture_mode_session()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kImage)); // Move the mouse over to capture label widget won't change the cursor since // it's a label not a label button. event_generator->MoveMouseTo(test_api.GetCaptureLabelWidget() ->GetWindowBoundsInScreen() .CenterPoint()); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kImage)); // Use pointer mouse if the event is on the capture bar. ClickOnView(GetVideoToggleButton(), event_generator); EXPECT_EQ(controller->type(), CaptureModeType::kVideo); EXPECT_EQ(CursorType::kPointer, cursor_manager->GetCursor().type()); EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); // Use video record icon as the mouse cursor icon in video recording mode. event_generator->MoveMouseTo(gfx::Point(175, 175)); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kVideo)); EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); // Enter tablet mode, the cursor should be hidden. // To avoid flaky failures due to mouse devices blocking entering tablet mode, // we detach all mouse devices. This shouldn't affect testing the cursor // status. SwitchToTabletMode(); EXPECT_FALSE(cursor_manager->IsCursorVisible()); EXPECT_TRUE(cursor_manager->IsCursorLocked()); // Exit tablet mode, the cursor should appear again. LeaveTabletMode(); EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kVideo)); EXPECT_TRUE(cursor_manager->IsCursorLocked()); // Stop capture mode, the cursor should be restored to its original state. controller->Stop(); EXPECT_FALSE(controller->IsActive()); EXPECT_FALSE(cursor_manager->IsCursorLocked()); EXPECT_EQ(original_cursor_type, cursor_manager->GetCursor().type()); // Test that if we're in tablet mode for dev purpose, the cursor should still // be visible. Shell::Get()->tablet_mode_controller()->SetEnabledForDev(true); StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); EXPECT_EQ(controller->type(), CaptureModeType::kImage); EXPECT_TRUE(cursor_manager->IsCursorLocked()); event_generator->MoveMouseTo(gfx::Point(175, 175)); EXPECT_TRUE(cursor_manager->IsCursorVisible()); } TEST_F(CaptureModeTest, WindowCursorStates) { using ui::mojom::CursorType; std::unique_ptr window(CreateTestWindow(gfx::Rect(200, 200))); auto* cursor_manager = Shell::Get()->cursor_manager(); CursorType original_cursor_type = cursor_manager->GetCursor().type(); EXPECT_FALSE(cursor_manager->IsCursorLocked()); EXPECT_EQ(CursorType::kPointer, original_cursor_type); auto* event_generator = GetEventGenerator(); CaptureModeController* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kImage); EXPECT_EQ(controller->type(), CaptureModeType::kImage); // If the mouse is above the window, use the image capture icon. event_generator->MoveMouseTo(gfx::Point(150, 150)); EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); CaptureModeSessionTestApi test_api(controller->capture_mode_session()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kImage)); // If the mouse is not above the window, use a pointer. event_generator->MoveMouseTo(gfx::Point(300, 300)); EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_EQ(CursorType::kPointer, cursor_manager->GetCursor().type()); // Use pointer mouse if the event is on the capture bar. ClickOnView(GetVideoToggleButton(), event_generator); EXPECT_EQ(controller->type(), CaptureModeType::kVideo); EXPECT_EQ(CursorType::kPointer, cursor_manager->GetCursor().type()); EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); // Use video record icon as the mouse cursor icon in video recording mode. event_generator->MoveMouseTo(gfx::Point(150, 150)); EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kVideo)); // If the mouse is not above the window, use the original mouse cursor. event_generator->MoveMouseTo(gfx::Point(300, 300)); EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_EQ(CursorType::kPointer, cursor_manager->GetCursor().type()); // Move above the window again, the cursor should change back to the video // record icon. event_generator->MoveMouseTo(gfx::Point(150, 150)); EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kVideo)); // Enter tablet mode, the cursor should be hidden. // To avoid flaky failures due to mouse devices blocking entering tablet mode, // we detach all mouse devices. This shouldn't affect testing the cursor // status. SwitchToTabletMode(); EXPECT_FALSE(cursor_manager->IsCursorVisible()); EXPECT_TRUE(cursor_manager->IsCursorLocked()); // Exit tablet mode, the cursor should appear again. LeaveTabletMode(); EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kVideo)); EXPECT_TRUE(cursor_manager->IsCursorLocked()); // Stop capture mode, the cursor should be restored to its original state. controller->Stop(); EXPECT_FALSE(controller->IsActive()); EXPECT_FALSE(cursor_manager->IsCursorLocked()); EXPECT_EQ(original_cursor_type, cursor_manager->GetCursor().type()); } // Tests that nothing crashes when windows are destroyed while being observed. TEST_F(CaptureModeTest, WindowDestruction) { using ui::mojom::CursorType; // Create 2 windows that overlap with each other. const gfx::Rect bounds1(0, 0, 200, 200); const gfx::Rect bounds2(150, 150, 200, 200); const gfx::Rect bounds3(50, 50, 200, 200); std::unique_ptr window1(CreateTestWindow(bounds1)); std::unique_ptr window2(CreateTestWindow(bounds2)); auto* cursor_manager = Shell::Get()->cursor_manager(); CursorType original_cursor_type = cursor_manager->GetCursor().type(); EXPECT_FALSE(cursor_manager->IsCursorLocked()); EXPECT_EQ(CursorType::kPointer, original_cursor_type); // Start capture session with Image type, so we have a custom cursor. auto* event_generator = GetEventGenerator(); CaptureModeController* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kImage); EXPECT_EQ(controller->type(), CaptureModeType::kImage); // If the mouse is above the window, use the image capture icon. event_generator->MoveMouseToCenterOf(window2.get()); EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); auto* capture_mode_session = controller->capture_mode_session(); CaptureModeSessionTestApi test_api(capture_mode_session); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kImage)); // Destroy the window while hovering. There is no window underneath, so it // should revert back to a pointer. window2.reset(); EXPECT_TRUE(cursor_manager->IsCursorLocked()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); EXPECT_EQ(CursorType::kPointer, cursor_manager->GetCursor().type()); // Destroy the window while mouse is in a pressed state. Cursor should revert // back to the original cursor. std::unique_ptr window3(CreateTestWindow(bounds2)); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kImage)); event_generator->PressLeftButton(); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kImage)); window3.reset(); event_generator->ReleaseLeftButton(); EXPECT_EQ(original_cursor_type, cursor_manager->GetCursor().type()); // When hovering over a window, if it is destroyed and there is another window // under the cursor location in screen, then the selected window is // automatically updated. std::unique_ptr window4(CreateTestWindow(bounds3)); event_generator->MoveMouseToCenterOf(window4.get()); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kImage)); EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window4.get()); window4.reset(); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kImage)); // Check to see it's observing window1. EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window1.get()); // Cursor is over a window in the mouse pressed state. If the window is // destroyed and there is another window under the cursor, the selected window // is updated and the new selected window is captured. std::unique_ptr window5(CreateTestWindow(bounds3)); EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window5.get()); event_generator->PressLeftButton(); window5.reset(); EXPECT_EQ(CursorType::kCustom, cursor_manager->GetCursor().type()); EXPECT_TRUE(test_api.IsUsingCustomCursor(CaptureModeType::kImage)); EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window1.get()); event_generator->ReleaseLeftButton(); EXPECT_FALSE(controller->IsActive()); } TEST_F(CaptureModeTest, CursorUpdatedOnDisplayRotation) { using ui::mojom::CursorType; UpdateDisplay("600x400"); const int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id(); display::SetInternalDisplayIds({display_id}); ScreenOrientationControllerTestApi orientation_test_api( Shell::Get()->screen_orientation_controller()); auto* event_generator = GetEventGenerator(); auto* cursor_manager = Shell::Get()->cursor_manager(); CaptureModeController* controller = StartCaptureSession( CaptureModeSource::kFullscreen, CaptureModeType::kImage); event_generator->MoveMouseTo(gfx::Point(175, 175)); EXPECT_TRUE(cursor_manager->IsCursorVisible()); // Use image capture icon as the mouse cursor icon in image capture mode. const ui::Cursor landscape_cursor = cursor_manager->GetCursor(); EXPECT_EQ(CursorType::kCustom, landscape_cursor.type()); CaptureModeSessionTestApi session_test_api( controller->capture_mode_session()); EXPECT_TRUE(session_test_api.IsUsingCustomCursor(CaptureModeType::kImage)); // Rotate the screen. orientation_test_api.SetDisplayRotation( display::Display::ROTATE_270, display::Display::RotationSource::ACTIVE); const ui::Cursor portrait_cursor = cursor_manager->GetCursor(); EXPECT_TRUE(session_test_api.IsUsingCustomCursor(CaptureModeType::kImage)); EXPECT_NE(landscape_cursor, portrait_cursor); } // Tests that in Region mode, cursor compositing is used instead of the system // cursor when the cursor is being dragged. TEST_F(CaptureModeTest, RegionDragCursorCompositing) { auto* event_generator = GetEventGenerator(); auto* session = StartImageRegionCapture()->capture_mode_session(); auto* cursor_manager = Shell::Get()->cursor_manager(); // Initially cursor should be visible and cursor compositing is not enabled. EXPECT_FALSE(session->is_drag_in_progress()); EXPECT_FALSE(IsCursorCompositingEnabled()); EXPECT_TRUE(cursor_manager->IsCursorVisible()); const gfx::Rect target_region(gfx::Rect(200, 200, 200, 200)); // For each start and end point try dragging and verify that cursor // compositing is functioning as expected. struct { std::string trace; gfx::Point start_point; gfx::Point end_point; } kDragCases[] = { {"initial_region", target_region.origin(), target_region.bottom_right()}, {"edge_resize", target_region.right_center(), gfx::Point(target_region.right_center() + gfx::Vector2d(50, 0))}, {"corner_resize", target_region.origin(), gfx::Point(175, 175)}, {"move", gfx::Point(250, 250), gfx::Point(300, 300)}, }; for (auto test_case : kDragCases) { SCOPED_TRACE(test_case.trace); event_generator->MoveMouseTo(test_case.start_point); event_generator->PressLeftButton(); EXPECT_TRUE(session->is_drag_in_progress()); EXPECT_TRUE(IsCursorCompositingEnabled()); event_generator->MoveMouseTo(test_case.end_point); EXPECT_TRUE(session->is_drag_in_progress()); EXPECT_TRUE(IsCursorCompositingEnabled()); event_generator->ReleaseLeftButton(); EXPECT_FALSE(session->is_drag_in_progress()); EXPECT_FALSE(IsCursorCompositingEnabled()); } } // Test that during countdown, capture mode session should not handle any // incoming input events. TEST_F(CaptureModeTest, DoNotHandleEventDuringCountDown) { // We need a non-zero duration to avoid infinite loop on countdown. ui::ScopedAnimationDurationScaleMode animation_scale( ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION); // Create 2 windows that overlap with each other. std::unique_ptr window1(CreateTestWindow(gfx::Rect(200, 200))); std::unique_ptr window2( CreateTestWindow(gfx::Rect(150, 150, 200, 200))); auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kWindow); controller->SetType(CaptureModeType::kVideo); controller->Start(CaptureModeEntryType::kQuickSettings); EXPECT_TRUE(controller->IsActive()); auto* event_generator = GetEventGenerator(); event_generator->MoveMouseToCenterOf(window1.get()); auto* capture_mode_session = controller->capture_mode_session(); EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window1.get()); // Start video recording. Countdown should start at this moment. event_generator->ClickLeftButton(); // Now move the mouse onto the other window, we should not change the captured // window during countdown. event_generator->MoveMouseToCenterOf(window2.get()); EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window1.get()); EXPECT_NE(capture_mode_session->GetSelectedWindow(), window2.get()); WaitForRecordingToStart(); } // Test that during countdown, window changes or crashes are handled. TEST_F(CaptureModeTest, WindowChangesDuringCountdown) { // We need a non-zero duration to avoid infinite loop on countdown. ui::ScopedAnimationDurationScaleMode animation_scale( ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION); std::unique_ptr window; auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kWindow); controller->SetType(CaptureModeType::kVideo); auto start_countdown = [this, &window, controller]() { window = CreateTestWindow(gfx::Rect(200, 200)); controller->Start(CaptureModeEntryType::kQuickSettings); auto* event_generator = GetEventGenerator(); event_generator->MoveMouseToCenterOf(window.get()); event_generator->ClickLeftButton(); EXPECT_TRUE(controller->IsActive()); EXPECT_FALSE(controller->is_recording_in_progress()); }; // Destroying or minimizing the observed window terminates the countdown and // exits capture mode. start_countdown(); window.reset(); EXPECT_FALSE(controller->IsActive()); start_countdown(); WindowState::Get(window.get())->Minimize(); EXPECT_FALSE(controller->IsActive()); // Activation changes (such as opening overview) should not terminate the // countdown. start_countdown(); EnterOverview(); EXPECT_TRUE(controller->IsActive()); EXPECT_FALSE(controller->is_recording_in_progress()); // Wait for countdown to finish and check that recording starts. WaitForRecordingToStart(); EXPECT_FALSE(controller->IsActive()); EXPECT_TRUE(controller->is_recording_in_progress()); } // Tests that metrics are recorded properly for capture mode entry points. TEST_F(CaptureModeTest, CaptureModeEntryPointHistograms) { constexpr char kClamshellHistogram[] = "Ash.CaptureModeController.EntryPoint.ClamshellMode"; constexpr char kTabletHistogram[] = "Ash.CaptureModeController.EntryPoint.TabletMode"; base::HistogramTester histogram_tester; auto* controller = CaptureModeController::Get(); // Test the various entry points in clamshell mode. controller->Start(CaptureModeEntryType::kAccelTakeWindowScreenshot); histogram_tester.ExpectBucketCount( kClamshellHistogram, CaptureModeEntryType::kAccelTakeWindowScreenshot, 1); controller->Stop(); controller->Start(CaptureModeEntryType::kAccelTakePartialScreenshot); histogram_tester.ExpectBucketCount( kClamshellHistogram, CaptureModeEntryType::kAccelTakePartialScreenshot, 1); controller->Stop(); controller->Start(CaptureModeEntryType::kQuickSettings); histogram_tester.ExpectBucketCount(kClamshellHistogram, CaptureModeEntryType::kQuickSettings, 1); controller->Stop(); controller->Start(CaptureModeEntryType::kStylusPalette); histogram_tester.ExpectBucketCount(kClamshellHistogram, CaptureModeEntryType::kStylusPalette, 1); controller->Stop(); // Enter tablet mode and test the various entry points in tablet mode. auto* tablet_mode_controller = Shell::Get()->tablet_mode_controller(); tablet_mode_controller->SetEnabledForTest(true); ASSERT_TRUE(tablet_mode_controller->InTabletMode()); controller->Start(CaptureModeEntryType::kAccelTakeWindowScreenshot); histogram_tester.ExpectBucketCount( kTabletHistogram, CaptureModeEntryType::kAccelTakeWindowScreenshot, 1); controller->Stop(); controller->Start(CaptureModeEntryType::kAccelTakePartialScreenshot); histogram_tester.ExpectBucketCount( kTabletHistogram, CaptureModeEntryType::kAccelTakePartialScreenshot, 1); controller->Stop(); controller->Start(CaptureModeEntryType::kQuickSettings); histogram_tester.ExpectBucketCount(kTabletHistogram, CaptureModeEntryType::kQuickSettings, 1); controller->Stop(); controller->Start(CaptureModeEntryType::kStylusPalette); histogram_tester.ExpectBucketCount(kTabletHistogram, CaptureModeEntryType::kStylusPalette, 1); controller->Stop(); // Check total counts for each histogram to ensure calls aren't counted in // multiple buckets. histogram_tester.ExpectTotalCount(kClamshellHistogram, 4); histogram_tester.ExpectTotalCount(kTabletHistogram, 4); // Check that histogram isn't counted if we don't actually enter capture mode. controller->Start(CaptureModeEntryType::kAccelTakePartialScreenshot); histogram_tester.ExpectBucketCount( kTabletHistogram, CaptureModeEntryType::kAccelTakePartialScreenshot, 2); controller->Start(CaptureModeEntryType::kAccelTakePartialScreenshot); histogram_tester.ExpectBucketCount( kTabletHistogram, CaptureModeEntryType::kAccelTakePartialScreenshot, 2); } // Verifies that the video notification will show the same thumbnail image as // sent by recording service. TEST_F(CaptureModeTest, VideoNotificationThumbnail) { auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); CaptureModeTestApi().FlushRecordingServiceForTesting(); auto* test_delegate = static_cast(controller->delegate_for_testing()); // Request and wait for a video frame so that the recording service can use it // to create a video thumbnail. test_delegate->RequestAndWaitForVideoFrame(); SkBitmap service_thumbnail = gfx::Image(test_delegate->GetVideoThumbnail()).AsBitmap(); EXPECT_FALSE(service_thumbnail.drawsNothing()); CaptureNotificationWaiter waiter; controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton); EXPECT_FALSE(controller->is_recording_in_progress()); waiter.Wait(); // Verify that the service's thumbnail is the same image shown in the // notification shown when recording ends. const message_center::Notification* notification = GetPreviewNotification(); EXPECT_TRUE(notification); EXPECT_FALSE(notification->image().IsEmpty()); const SkBitmap notification_thumbnail = notification->image().AsBitmap(); EXPECT_TRUE( gfx::test::AreBitmapsEqual(notification_thumbnail, service_thumbnail)); } TEST_F(CaptureModeTest, LowDriveFsSpace) { auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); const base::FilePath drive_fs_folder = CreateFolderOnDriveFS("test"); controller->SetCustomCaptureFolder(drive_fs_folder); auto* test_delegate = static_cast(controller->delegate_for_testing()); // Simulate low DriveFS free space by setting it to e.g. 200 MB. test_delegate->set_fake_drive_fs_free_bytes(200 * 1024 * 1024); base::HistogramTester histogram_tester; StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); CaptureModeTestApi().FlushRecordingServiceForTesting(); test_delegate->RequestAndWaitForVideoFrame(); // Recording should end immediately due to a low Drive FS free space. WaitForCaptureFileToBeSaved(); histogram_tester.ExpectBucketCount( kEndRecordingReasonInClamshellHistogramName, EndRecordingReason::kLowDriveFsQuota, 1); } TEST_F(CaptureModeTest, WindowRecordingCaptureId) { auto window = CreateTestWindow(gfx::Rect(200, 200)); StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo); auto* event_generator = GetEventGenerator(); event_generator->MoveMouseToCenterOf(window.get()); auto* controller = CaptureModeController::Get(); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); // The window should have a valid capture ID. EXPECT_TRUE(window->subtree_capture_id().is_valid()); // Once recording ends, the window should no longer be marked as capturable. controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton); EXPECT_FALSE(controller->is_recording_in_progress()); EXPECT_FALSE(window->subtree_capture_id().is_valid()); } TEST_F(CaptureModeTest, ClosingDimmedWidgetAboveRecordedWindow) { views::Widget* widget = TestWidgetBuilder().BuildOwnedByNativeWidget(); auto* window = widget->GetNativeWindow(); auto recorded_window = CreateTestWindow(gfx::Rect(200, 200)); auto* controller = StartSessionAndRecordWindow(recorded_window.get()); EXPECT_TRUE(controller->is_recording_in_progress()); auto* recording_watcher = controller->video_recording_watcher_for_testing(); // Activate the window so that it becomes on top of the recorded window, and // expect it gets dimmed. wm::ActivateWindow(window); EXPECT_TRUE(recording_watcher->IsWindowDimmedForTesting(window)); // Close the widget, this should not lead to any use-after-free. See // https://crbug.com/1273197. widget->Close(); } TEST_F(CaptureModeTest, DimmingOfUnRecordedWindows) { auto win1 = CreateTestWindow(gfx::Rect(200, 200)); auto win2 = CreateTestWindow(gfx::Rect(200, 200)); auto recorded_window = CreateTestWindow(gfx::Rect(200, 200)); auto* controller = StartSessionAndRecordWindow(recorded_window.get()); auto* recording_watcher = controller->video_recording_watcher_for_testing(); auto* shield_layer = recording_watcher->layer(); // Since the recorded window is the top most, no windows should be // individually dimmed. EXPECT_TRUE(recording_watcher->should_paint_layer()); EXPECT_TRUE(IsLayerStackedRightBelow(shield_layer, recorded_window->layer())); EXPECT_FALSE( recording_watcher->IsWindowDimmedForTesting(recorded_window.get())); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(win1.get())); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(win2.get())); // Activating |win1| brings it to the front of the shield, so it should be // dimmed separately. wm::ActivateWindow(win1.get()); EXPECT_TRUE(recording_watcher->should_paint_layer()); EXPECT_TRUE(IsLayerStackedRightBelow(shield_layer, recorded_window->layer())); EXPECT_FALSE( recording_watcher->IsWindowDimmedForTesting(recorded_window.get())); EXPECT_TRUE(recording_watcher->IsWindowDimmedForTesting(win1.get())); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(win2.get())); // Similarly for |win2|. wm::ActivateWindow(win2.get()); EXPECT_TRUE(recording_watcher->should_paint_layer()); EXPECT_TRUE(IsLayerStackedRightBelow(shield_layer, recorded_window->layer())); EXPECT_FALSE( recording_watcher->IsWindowDimmedForTesting(recorded_window.get())); EXPECT_TRUE(recording_watcher->IsWindowDimmedForTesting(win1.get())); EXPECT_TRUE(recording_watcher->IsWindowDimmedForTesting(win2.get())); // Minimizing the recorded window should stop painting the shield, and the // dimmers should be removed. WindowState::Get(recorded_window.get())->Minimize(); EXPECT_FALSE(recording_watcher->should_paint_layer()); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(win1.get())); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(win2.get())); // Activating the recorded window again unminimizes the window, which will // reenable painting the shield. wm::ActivateWindow(recorded_window.get()); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(win1.get())); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(win2.get())); EXPECT_FALSE(WindowState::Get(recorded_window.get())->IsMinimized()); EXPECT_TRUE(recording_watcher->should_paint_layer()); // Destroying a dimmed window is correctly tracked. wm::ActivateWindow(win2.get()); EXPECT_TRUE(recording_watcher->IsWindowDimmedForTesting(win2.get())); win2.reset(); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(win2.get())); } TEST_F(CaptureModeTest, DimmingWithDesks) { auto recorded_window = CreateAppWindow(gfx::Rect(250, 100)); auto* controller = StartSessionAndRecordWindow(recorded_window.get()); auto* recording_watcher = controller->video_recording_watcher_for_testing(); EXPECT_TRUE(recording_watcher->should_paint_layer()); auto* desks_controller = DesksController::Get(); desks_controller->NewDesk(DesksCreationRemovalSource::kKeyboard); Desk* desk_2 = desks_controller->desks()[1].get(); ActivateDesk(desk_2); // A window on a different desk than that of the recorded window should not be // dimmed. auto win1 = CreateAppWindow(gfx::Rect(200, 200)); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(win1.get())); // However, moving it to the desk of the recorded window should give it a // dimmer, since it's a more recently-used window (i.e. above the recorded // window). Desk* desk_1 = desks_controller->desks()[0].get(); desks_controller->MoveWindowFromActiveDeskTo( win1.get(), desk_1, win1->GetRootWindow(), DesksMoveWindowFromActiveDeskSource::kShortcut); EXPECT_TRUE(recording_watcher->IsWindowDimmedForTesting(win1.get())); // Moving the recorded window out of the active desk should destroy the // dimmer. ActivateDesk(desk_1); desks_controller->MoveWindowFromActiveDeskTo( recorded_window.get(), desk_2, recorded_window->GetRootWindow(), DesksMoveWindowFromActiveDeskSource::kShortcut); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(win1.get())); } TEST_F(CaptureModeTest, DimmingWithDisplays) { UpdateDisplay("500x400,401+0-800x700"); auto recorded_window = CreateAppWindow(gfx::Rect(250, 100)); auto* controller = StartSessionAndRecordWindow(recorded_window.get()); auto* recording_watcher = controller->video_recording_watcher_for_testing(); EXPECT_TRUE(recording_watcher->should_paint_layer()); // Create a new window on the second display. It should not be dimmed. auto window = CreateTestWindow(gfx::Rect(420, 10, 200, 200)); auto roots = Shell::GetAllRootWindows(); EXPECT_EQ(roots[1], window->GetRootWindow()); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(window.get())); // However when moved to the first display, it gets dimmed. window_util::MoveWindowToDisplay(window.get(), roots[0]->GetHost()->GetDisplayId()); EXPECT_TRUE(recording_watcher->IsWindowDimmedForTesting(window.get())); // Moving the recorded window to the second display will remove the dimming of // |window|. window_util::MoveWindowToDisplay(recorded_window.get(), roots[1]->GetHost()->GetDisplayId()); EXPECT_FALSE(recording_watcher->IsWindowDimmedForTesting(window.get())); } TEST_F(CaptureModeTest, MultiDisplayWindowRecording) { UpdateDisplay("500x400,401+0-800x700"); auto roots = Shell::GetAllRootWindows(); ASSERT_EQ(2u, roots.size()); auto window = CreateTestWindow(gfx::Rect(200, 200)); auto* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo); auto* event_generator = GetEventGenerator(); event_generator->MoveMouseToCenterOf(window.get()); auto* session_layer = controller->capture_mode_session()->layer(); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); // The session layer is reused to paint the recording shield. auto* shield_layer = controller->video_recording_watcher_for_testing()->layer(); EXPECT_EQ(session_layer, shield_layer); EXPECT_EQ(shield_layer->parent(), window->layer()->parent()); EXPECT_TRUE(IsLayerStackedRightBelow(shield_layer, window->layer())); EXPECT_EQ(shield_layer->bounds(), roots[0]->bounds()); // The capturer should capture from the frame sink of the first display. // The video size should match the window's size. CaptureModeTestApi test_api; test_api.FlushRecordingServiceForTesting(); auto* test_delegate = static_cast(controller->delegate_for_testing()); EXPECT_EQ(roots[0]->GetFrameSinkId(), test_delegate->GetCurrentFrameSinkId()); EXPECT_EQ(roots[0]->bounds().size(), test_delegate->GetCurrentFrameSinkSizeInPixels()); EXPECT_EQ(window->bounds().size(), test_delegate->GetCurrentVideoSize()); // Moving a window to a different display should be propagated to the service, // with the new root's frame sink ID, and the new root's size. window_util::MoveWindowToDisplay(window.get(), roots[1]->GetHost()->GetDisplayId()); test_api.FlushRecordingServiceForTesting(); ASSERT_EQ(window->GetRootWindow(), roots[1]); EXPECT_EQ(roots[1]->GetFrameSinkId(), test_delegate->GetCurrentFrameSinkId()); EXPECT_EQ(roots[1]->bounds().size(), test_delegate->GetCurrentFrameSinkSizeInPixels()); EXPECT_EQ(window->bounds().size(), test_delegate->GetCurrentVideoSize()); // The shield layer should move with the window, and maintain the stacking // below the window's layer. EXPECT_EQ(shield_layer->parent(), window->layer()->parent()); EXPECT_TRUE(IsLayerStackedRightBelow(shield_layer, window->layer())); EXPECT_EQ(shield_layer->bounds(), roots[1]->bounds()); } // Flaky especially on MSan: https://crbug.com/1293188 TEST_F(CaptureModeTest, DISABLED_WindowResizing) { UpdateDisplay("700x600"); auto window = CreateTestWindow(gfx::Rect(200, 200)); auto* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo); auto* event_generator = GetEventGenerator(); event_generator->MoveMouseToCenterOf(window.get()); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); auto* test_delegate = static_cast(controller->delegate_for_testing()); CaptureModeTestApi test_api; test_api.FlushRecordingServiceForTesting(); EXPECT_EQ(gfx::Size(200, 200), test_delegate->GetCurrentVideoSize()); EXPECT_EQ(gfx::Size(700, 600), test_delegate->GetCurrentFrameSinkSizeInPixels()); // Multiple resize events should be throttled. window->SetBounds(gfx::Rect(250, 250)); test_api.FlushRecordingServiceForTesting(); EXPECT_EQ(gfx::Size(200, 200), test_delegate->GetCurrentVideoSize()); window->SetBounds(gfx::Rect(250, 300)); test_api.FlushRecordingServiceForTesting(); EXPECT_EQ(gfx::Size(200, 200), test_delegate->GetCurrentVideoSize()); window->SetBounds(gfx::Rect(300, 300)); test_api.FlushRecordingServiceForTesting(); EXPECT_EQ(gfx::Size(200, 200), test_delegate->GetCurrentVideoSize()); // Once throttling ends, the current size is pushed. auto* recording_watcher = controller->video_recording_watcher_for_testing(); recording_watcher->SendThrottledWindowSizeChangedNowForTesting(); test_api.FlushRecordingServiceForTesting(); EXPECT_EQ(gfx::Size(300, 300), test_delegate->GetCurrentVideoSize()); EXPECT_EQ(gfx::Size(700, 600), test_delegate->GetCurrentFrameSinkSizeInPixels()); // Maximizing a window changes its size, and is pushed to the service with // throttling. WindowState::Get(window.get())->Maximize(); test_api.FlushRecordingServiceForTesting(); EXPECT_EQ(gfx::Size(300, 300), test_delegate->GetCurrentVideoSize()); recording_watcher->SendThrottledWindowSizeChangedNowForTesting(); test_api.FlushRecordingServiceForTesting(); EXPECT_NE(gfx::Size(300, 300), test_delegate->GetCurrentVideoSize()); EXPECT_EQ(window->bounds().size(), test_delegate->GetCurrentVideoSize()); } TEST_F(CaptureModeTest, RotateDisplayWhileRecording) { UpdateDisplay("600x800"); auto* controller = StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo); SelectRegion(gfx::Rect(20, 40, 100, 200)); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); // Initially the frame sink size matches the un-rotated display size in DIPs, // but the video size matches the size of the crop region. CaptureModeTestApi test_api; test_api.FlushRecordingServiceForTesting(); auto* test_delegate = static_cast(controller->delegate_for_testing()); EXPECT_EQ(gfx::Size(600, 800), test_delegate->GetCurrentFrameSinkSizeInPixels()); EXPECT_EQ(gfx::Size(100, 200), test_delegate->GetCurrentVideoSize()); // Rotate by 90 degree, the frame sink size should be updated to match that. // The video size should remain unaffected. Shell::Get()->display_manager()->SetDisplayRotation( WindowTreeHostManager::GetPrimaryDisplayId(), display::Display::ROTATE_90, display::Display::RotationSource::USER); test_api.FlushRecordingServiceForTesting(); EXPECT_EQ(gfx::Size(800, 600), test_delegate->GetCurrentFrameSinkSizeInPixels()); EXPECT_EQ(gfx::Size(100, 200), test_delegate->GetCurrentVideoSize()); } // Regression test for https://crbug.com/1331095. TEST_F(CaptureModeTest, CornerRegionWithScreenRotation) { UpdateDisplay("800x600"); // Pick a region at the bottom right corner of the landscape screen, so that // when the screen is rotated to portrait, the unadjusted region becomes // outside the new portrait bounds. auto* controller = StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kVideo); SelectRegion(gfx::Rect(700, 400, 100, 200)); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); CaptureModeTestApi test_api; test_api.FlushRecordingServiceForTesting(); auto* test_delegate = static_cast(controller->delegate_for_testing()); EXPECT_EQ(gfx::Size(100, 200), test_delegate->GetCurrentVideoSize()); auto* root_window = Shell::GetPrimaryRootWindow(); auto* recording_watcher = controller->video_recording_watcher_for_testing(); gfx::Rect effective_region_bounds = recording_watcher->GetEffectivePartialRegionBounds(); EXPECT_FALSE(effective_region_bounds.IsEmpty()); EXPECT_TRUE(root_window->bounds().Contains(effective_region_bounds)); // Verifies that the bounds of the visible rect of the frame is within the // bounds of the root window. auto verify_video_frame = [&](const media::VideoFrame& frame, const gfx::Rect& content_rect) { EXPECT_TRUE(root_window->bounds().Contains(frame.visible_rect())); }; test_delegate->recording_service()->RequestAndWaitForVideoFrame( base::BindLambdaForTesting(verify_video_frame)); // Rotate by 90 degree, the adjusted region bounds should not be empty and // should remain within the bounds of the new portrait root window bounds. Shell::Get()->display_manager()->SetDisplayRotation( WindowTreeHostManager::GetPrimaryDisplayId(), display::Display::ROTATE_90, display::Display::RotationSource::USER); test_api.FlushRecordingServiceForTesting(); EXPECT_EQ(gfx::Size(100, 200), test_delegate->GetCurrentVideoSize()); effective_region_bounds = recording_watcher->GetEffectivePartialRegionBounds(); EXPECT_FALSE(effective_region_bounds.IsEmpty()); EXPECT_TRUE(root_window->bounds().Contains(effective_region_bounds)); test_delegate->recording_service()->RequestAndWaitForVideoFrame( base::BindLambdaForTesting(verify_video_frame)); } // Tests that the video frames delivered to the service for recorded windows are // valid (i.e. they have the correct size, and suffer from no letterboxing, even // when the window gets resized). // This is a regression test for https://crbug.com/1214023. TEST_F(CaptureModeTest, VerifyWindowRecordingVideoFrames) { auto window = CreateTestWindow(gfx::Rect(100, 50, 200, 200)); StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo); auto* event_generator = GetEventGenerator(); event_generator->MoveMouseToCenterOf(window.get()); auto* controller = CaptureModeController::Get(); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); CaptureModeTestApi test_api; test_api.FlushRecordingServiceForTesting(); bool is_video_frame_valid = false; std::string failures; auto verify_video_frame = [&](const media::VideoFrame& frame, const gfx::Rect& content_rect) { is_video_frame_valid = true; failures.clear(); // Having the content positioned at (0,0) with a size that matches the // current window's size means that there is no letterboxing. if (gfx::Point() != content_rect.origin()) { is_video_frame_valid = false; failures = base::StringPrintf("content_rect is not at (0,0), instead at: %s\n", content_rect.origin().ToString().c_str()); } const gfx::Size window_size = window->bounds().size(); if (window_size != content_rect.size()) { is_video_frame_valid = false; failures += base::StringPrintf( "content_rect doesn't match the window size:\n" " content_rect.size(): %s\n" " window_size: %s\n", content_rect.size().ToString().c_str(), window_size.ToString().c_str()); } // The video frame contents should match the bounds of the video frame. if (frame.visible_rect() != content_rect) { is_video_frame_valid = false; failures += base::StringPrintf( "content_rect doesn't match the frame's visible_rect:\n" " content_rect: %s\n" " visible_rect: %s\n", content_rect.ToString().c_str(), frame.visible_rect().ToString().c_str()); } if (frame.coded_size() != window_size) { is_video_frame_valid = false; failures += base::StringPrintf( "the frame's coded size doesn't match the window size:\n" " frame.coded_size(): %s\n" " window_size: %s\n", frame.coded_size().ToString().c_str(), window_size.ToString().c_str()); } }; auto* test_delegate = static_cast(controller->delegate_for_testing()); ASSERT_TRUE(test_delegate->recording_service()); { SCOPED_TRACE("Initial window size"); test_delegate->recording_service()->RequestAndWaitForVideoFrame( base::BindLambdaForTesting(verify_video_frame)); EXPECT_TRUE(is_video_frame_valid) << failures; } // Even when the window is resized and the throttled size reaches the service, // new video frames should still be valid. window->SetBounds(gfx::Rect(120, 60, 600, 500)); auto* recording_watcher = controller->video_recording_watcher_for_testing(); recording_watcher->SendThrottledWindowSizeChangedNowForTesting(); test_api.FlushRecordingServiceForTesting(); { SCOPED_TRACE("After window resizing"); // A video frame is produced on the Viz side when a CopyOutputRequest is // fulfilled. Those CopyOutputRequests could have been placed before the // window's layer resize results in a new resized render pass in Viz. But // eventually this must happen, and a valid frame must be delivered. int remaining_attempts = 2; do { --remaining_attempts; test_delegate->recording_service()->RequestAndWaitForVideoFrame( base::BindLambdaForTesting(verify_video_frame)); } while (!is_video_frame_valid && remaining_attempts); EXPECT_TRUE(is_video_frame_valid) << failures; } controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton); EXPECT_FALSE(controller->is_recording_in_progress()); } // Tests that the focus should be on the `Settings` button after closing the // settings menu. TEST_F(CaptureModeTest, ReturnFocusToSettingsButtonAfterSettingsMenuIsClosed) { auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); CaptureModeSession* capture_mode_session = controller->capture_mode_session(); CaptureModeSessionTestApi test_api(capture_mode_session); using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; auto* event_generator = GetEventGenerator(); // Check the initial focus of the focus ring. EXPECT_EQ(FocusGroup::kNone, test_api.GetCurrentFocusGroup()); // Tab six times, `Settings` button should be focused. SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/6); EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup()); EXPECT_TRUE(test_api.GetCaptureModeBarView()->settings_button()->has_focus()); // Tab the space key and the settings menu will be opened. SendKey(ui::VKEY_SPACE, event_generator, ui::EF_NONE); EXPECT_TRUE(test_api.GetCaptureModeSettingsView()); EXPECT_EQ(FocusGroup::kPendingSettings, test_api.GetCurrentFocusGroup()); // Close the settings menu, the focus ring should be on the `Settings` button. SendKey(ui::VKEY_ESCAPE, event_generator, ui::EF_NONE); EXPECT_FALSE(test_api.GetCaptureModeSettingsView()); EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup()); EXPECT_TRUE(test_api.GetCaptureModeBarView()->settings_button()->has_focus()); // Tab the space key to open the settings menu again and tab to focus on the // settings menu item. SendKey(ui::VKEY_SPACE, event_generator, ui::EF_NONE); EXPECT_TRUE(test_api.GetCaptureModeSettingsView()); SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/3); EXPECT_EQ(FocusGroup::kSettingsMenu, test_api.GetCurrentFocusGroup()); // Close the settings menu, the focus ring should be on the `Settings` button. SendKey(ui::VKEY_ESCAPE, event_generator, ui::EF_NONE); EXPECT_FALSE(test_api.GetCaptureModeSettingsView()); EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup()); EXPECT_TRUE(test_api.GetCaptureModeBarView()->settings_button()->has_focus()); } // Tests that minimized window(s) will be ignored whereas four corners occluded // but overall partially occluded window will be focusable while tabbing through // in `kWindow` mode. TEST_F(CaptureModeTest, IgnoreMinimizeWindowsInKWindow) { // Layout of three windows: four corners of `window3` are occluded by // `window1` and `window2`. // // +------+ // | | +-----------+ // | 1 |-------| | // | | 3 | 2 | // | | | | // | | | | // | |-------| | // | | +-----------+ // +------+ std::unique_ptr window3 = CreateTestWindow(gfx::Rect(100, 45, 150, 200)); std::unique_ptr window2 = CreateTestWindow(gfx::Rect(150, 50, 150, 250)); std::unique_ptr window1 = CreateTestWindow(gfx::Rect(20, 30, 100, 300)); std::unique_ptr window4( CreateTestWindow(gfx::Rect(0, 0, 50, 90))); WindowState::Get(window4.get())->Minimize(); auto* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kImage); CaptureModeSession* capture_mode_session = controller->capture_mode_session(); CaptureModeSessionTestApi test_api(capture_mode_session); using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; auto* event_generator = GetEventGenerator(); EXPECT_EQ(FocusGroup::kNone, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); // Tab six times, `window1` should be focused. Tab another time, `window2` // should be focused. Tab again, `window3` will be focused. SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/6); EXPECT_EQ(FocusGroup::kCaptureWindow, test_api.GetCurrentFocusGroup()); EXPECT_EQ(window1.get(), capture_mode_session->GetSelectedWindow()); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(window2.get(), capture_mode_session->GetSelectedWindow()); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(window3.get(), capture_mode_session->GetSelectedWindow()); // Tab once, the `settings` button should be focused. The minimized `window4` // will be ignored during the tabbing process. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup()); controller->Stop(); } // Tests that partially occluded window(s) will be focusable even when four // edges are occluded by other windows while tabbing through in `kWindow` mode. TEST_F(CaptureModeTest, PartiallyOccludedWindowIsFocusableInKWindow) { // Layout of five windows: four edges of `window3` is occluded by `window1`, // `window2`, `window4` and `window5` respectively, but the middle part is not // occluded. // +-----------+ // | | // +----| 4 | // | | |---------+ // | | | | // | +-|-------|-+ | // | 1 | 3 | 2 | // | | | | // | +-|-------|--+ | // | | |--------+ // +----| 5 | // | | // +------------+ std::unique_ptr window3 = CreateTestWindow(gfx::Rect(100, 45, 150, 200)); std::unique_ptr window2 = CreateTestWindow(gfx::Rect(150, 50, 150, 250)); std::unique_ptr window1 = CreateTestWindow(gfx::Rect(20, 30, 100, 300)); std::unique_ptr window4 = CreateTestWindow(gfx::Rect(50, 5, 150, 55)); std::unique_ptr window5 = CreateTestWindow(gfx::Rect(60, 225, 210, 45)); auto* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kImage); CaptureModeSession* capture_mode_session = controller->capture_mode_session(); CaptureModeSessionTestApi test_api(capture_mode_session); using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; auto* event_generator = GetEventGenerator(); EXPECT_EQ(FocusGroup::kNone, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); // Tab six times, `window5` should be focused. Then `window4`, `window1`, // `window2` and `window3` will be focused after each tab. SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/6); EXPECT_EQ(FocusGroup::kCaptureWindow, test_api.GetCurrentFocusGroup()); EXPECT_EQ(window5.get(), capture_mode_session->GetSelectedWindow()); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(window4.get(), capture_mode_session->GetSelectedWindow()); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(window1.get(), capture_mode_session->GetSelectedWindow()); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(window2.get(), capture_mode_session->GetSelectedWindow()); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(window3.get(), capture_mode_session->GetSelectedWindow()); // Tab once, the `settings` button should be focused. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup()); controller->Stop(); } // Tests that fully occluded window(s) will be ignored while tabbing in // `kWindow`. TEST_F(CaptureModeTest, IgnoreFullyOccludedWindowWhileTabbingInKWindow) { // Layout of six windows: `window3` is fully occluded by `window1`, `window2`, // `window4`, `window5` and `window6`. // +-----------+ // | | // +----| 4 | // | 1 | |---------+ // | +-----------------+ | // | | | | // | | 6 | 2 | // | | | | // | +-----------------+ | // | | |--------+ // +----| 5 | // | | // +------------+ std::unique_ptr window3 = CreateTestWindow(gfx::Rect(100, 45, 150, 200)); std::unique_ptr window2 = CreateTestWindow(gfx::Rect(150, 50, 150, 250)); std::unique_ptr window1 = CreateTestWindow(gfx::Rect(20, 30, 100, 300)); std::unique_ptr window4 = CreateTestWindow(gfx::Rect(50, 5, 150, 55)); std::unique_ptr window5 = CreateTestWindow(gfx::Rect(60, 225, 210, 45)); std::unique_ptr window6 = CreateTestWindow(gfx::Rect(30, 55, 175, 185)); auto* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kImage); CaptureModeSession* capture_mode_session = controller->capture_mode_session(); CaptureModeSessionTestApi test_api(capture_mode_session); using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; auto* event_generator = GetEventGenerator(); EXPECT_EQ(FocusGroup::kNone, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); // Tab six times, `window6` should be focused. Then `window5`, `window4`, // `window1` and `window2` will be focused after each tab. SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/6); EXPECT_EQ(FocusGroup::kCaptureWindow, test_api.GetCurrentFocusGroup()); EXPECT_EQ(window6.get(), capture_mode_session->GetSelectedWindow()); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(window5.get(), capture_mode_session->GetSelectedWindow()); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(window4.get(), capture_mode_session->GetSelectedWindow()); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(window1.get(), capture_mode_session->GetSelectedWindow()); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(window2.get(), capture_mode_session->GetSelectedWindow()); // Tab once, the `settings` button should be focused. The fully occluded // `window3` will be ignored during the tabbing process. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup()); } class CaptureModeSaveFileTest : public CaptureModeTest, public testing::WithParamInterface { public: CaptureModeSaveFileTest() = default; CaptureModeSaveFileTest( const CaptureModeSaveFileTest& capture_mode_save_file_test) = delete; CaptureModeSaveFileTest& operator=(const CaptureModeSaveFileTest&) = delete; ~CaptureModeSaveFileTest() override = default; void StartCaptureSessionWithParam() { StartCaptureSession(CaptureModeSource::kFullscreen, GetParam()); } // Based on the `CaptureModeType`, it performs the capture and then returns // the path of the saved image or video files. base::FilePath PerformCapture() { auto* controller = CaptureModeController::Get(); switch (GetParam()) { case CaptureModeType::kImage: controller->PerformCapture(); return WaitForCaptureFileToBeSaved(); case CaptureModeType::kVideo: StartVideoRecordingImmediately(); controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton); return WaitForCaptureFileToBeSaved(); } } }; // Tests that if the custom folder becomes unavailable, the captured file should // be saved into the default folder. Otherwise, it's saved into custom folder. TEST_P(CaptureModeSaveFileTest, SaveCapturedFileWithCustomFolder) { auto* controller = CaptureModeController::Get(); const base::FilePath default_folder = controller->delegate_for_testing()->GetUserDefaultDownloadsFolder(); const base::FilePath custom_folder((FILE_PATH_LITERAL("/home/tests"))); controller->SetCustomCaptureFolder(custom_folder); // Make sure the current folder is the custom folder here and then perform // capture. auto capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_FALSE(capture_folder.is_default_downloads_folder); StartCaptureSessionWithParam(); base::FilePath file_saved_path = PerformCapture(); // Since `custom_folder` is not available, the captured files will be saved // into default folder; EXPECT_EQ(file_saved_path.DirName(), default_folder); // Now create an available custom folder and set it for custom capture folder. const base::FilePath available_custom_folder = CreateCustomFolderInUserDownloadsPath("test"); controller->SetCustomCaptureFolder(available_custom_folder); capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_FALSE(capture_folder.is_default_downloads_folder); StartCaptureSessionWithParam(); file_saved_path = PerformCapture(); // Since `available_custom_folder` is now available, the captured files will // be saved into the custom folder; EXPECT_EQ(file_saved_path.DirName(), available_custom_folder); } TEST_P(CaptureModeSaveFileTest, CaptureModeSaveToLocationMetric) { constexpr char kHistogramBase[] = "Ash.CaptureModeController.SaveLocation"; base::HistogramTester histogram_tester; auto* controller = CaptureModeController::Get(); auto* test_delegate = controller->delegate_for_testing(); // Initialize four different save-to locations for screen capture that // includes default downloads folder, local customized folder, root drive and // a specific folder on drive. const auto downloads_folder = test_delegate->GetUserDefaultDownloadsFolder(); const base::FilePath custom_folder = CreateCustomFolderInUserDownloadsPath("test"); base::FilePath mount_point_path; test_delegate->GetDriveFsMountPointPath(&mount_point_path); const auto root_drive_folder = mount_point_path.Append("root"); const base::FilePath non_root_drive_folder = CreateFolderOnDriveFS("test"); struct { base::FilePath set_save_file_folder; CaptureModeSaveToLocation save_location; } kTestCases[] = { {downloads_folder, CaptureModeSaveToLocation::kDefault}, {custom_folder, CaptureModeSaveToLocation::kCustomizedFolder}, {root_drive_folder, CaptureModeSaveToLocation::kDrive}, {non_root_drive_folder, CaptureModeSaveToLocation::kDriveFolder}, }; for (auto test_case : kTestCases) { histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramBase), test_case.save_location, 0); } // Set four different save-to locations in clamshell mode and check the // histogram results. EXPECT_FALSE(Shell::Get()->IsInTabletMode()); for (auto test_case : kTestCases) { StartCaptureSessionWithParam(); controller->SetCustomCaptureFolder(test_case.set_save_file_folder); auto file_saved_path = PerformCapture(); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramBase), test_case.save_location, 1); } // Set four different save-to locations in tablet mode and check the histogram // results. SwitchToTabletMode(); EXPECT_TRUE(Shell::Get()->IsInTabletMode()); for (auto test_case : kTestCases) { StartCaptureSessionWithParam(); controller->SetCustomCaptureFolder(test_case.set_save_file_folder); auto file_saved_path = PerformCapture(); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramBase), test_case.save_location, 1); } } INSTANTIATE_TEST_SUITE_P(All, CaptureModeSaveFileTest, testing::Values(CaptureModeType::kImage, CaptureModeType::kVideo)); // Test fixture for verifying that the videos are recorded at the pixel size of // the targets being captured in all recording modes. This avoids having the // scaling in CopyOutputRequests when performing the capture at a different size // than that of the render pass (which is in pixels). This scaling causes a loss // of quality, and a blurry video frames. https://crbug.com/1215185. class CaptureModeRecordingSizeTest : public CaptureModeTest { public: CaptureModeRecordingSizeTest() = default; ~CaptureModeRecordingSizeTest() override = default; // CaptureModeTest: void SetUp() override { CaptureModeTest::SetUp(); window_ = CreateTestWindow(gfx::Rect(100, 50, 200, 200)); CaptureModeController::Get()->SetUserCaptureRegion(user_region_, /*by_user=*/true); UpdateDisplay("800x600"); } void TearDown() override { window_.reset(); CaptureModeTest::TearDown(); } // Converts the given |size| from DIPs to pixels based on the given value of // |dsf|. gfx::Size ToPixels(const gfx::Size& size, float dsf) const { return gfx::ToFlooredSize(gfx::ConvertSizeToPixels(size, dsf)); } protected: // Verifies the size of the received video frame. static void VerifyVideoFrame(const gfx::Size& expected_video_size, const media::VideoFrame& frame, const gfx::Rect& content_rect) { // The I420 pixel format does not like odd dimensions, so the size of the // visible rect in the video frame will be adjusted to be an even value. const gfx::Size adjusted_size(expected_video_size.width() & ~1, expected_video_size.height() & ~1); EXPECT_EQ(adjusted_size, frame.visible_rect().size()); } CaptureModeController* StartVideoRecording(CaptureModeSource source) { auto* controller = StartCaptureSession(source, CaptureModeType::kVideo); if (source == CaptureModeSource::kWindow) GetEventGenerator()->MoveMouseToCenterOf(window_.get()); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); CaptureModeTestApi().FlushRecordingServiceForTesting(); return controller; } void SetDeviceScaleFactor(float dsf) { const auto display_id = display_manager()->GetDisplayAt(0).id(); display_manager()->UpdateZoomFactor(display_id, dsf); EXPECT_EQ(dsf, window_->GetHost()->device_scale_factor()); auto* controller = CaptureModeController::Get(); if (controller->is_recording_in_progress()) { CaptureModeTestApi().FlushRecordingServiceForTesting(); auto* test_delegate = static_cast( controller->delegate_for_testing()); // Consume any pending video frame from before changing the DSF prior to // proceeding. test_delegate->RequestAndWaitForVideoFrame(); } } protected: const gfx::Rect user_region_{20, 50}; std::unique_ptr window_; }; // TODO(crbug.com/1291073): Flaky on ChromeOS. #if BUILDFLAG(IS_CHROMEOS) #define MAYBE_CaptureAtPixelsFullscreen DISABLED_CaptureAtPixelsFullscreen #else #define MAYBE_CaptureAtPixelsFullscreen CaptureAtPixelsFullscreen #endif TEST_F(CaptureModeRecordingSizeTest, MAYBE_CaptureAtPixelsFullscreen) { float dsf = 1.6f; SetDeviceScaleFactor(dsf); auto* controller = StartVideoRecording(CaptureModeSource::kFullscreen); auto* root = window_->GetRootWindow(); gfx::Size initial_root_window_size_pixels = ToPixels(root->bounds().size(), dsf); auto* test_delegate = static_cast(controller->delegate_for_testing()); ASSERT_TRUE(test_delegate->recording_service()); { SCOPED_TRACE("Testing @ 1.6 device scale factor"); EXPECT_EQ(initial_root_window_size_pixels, test_delegate->GetCurrentVideoSize()); EXPECT_EQ( dsf, test_delegate->recording_service()->GetCurrentDeviceScaleFactor()); EXPECT_EQ(initial_root_window_size_pixels, test_delegate->GetCurrentFrameSinkSizeInPixels()); test_delegate->recording_service()->RequestAndWaitForVideoFrame( base::BindOnce(&CaptureModeRecordingSizeTest::VerifyVideoFrame, initial_root_window_size_pixels)); } // Change the DSF and expect the video size will remain at the initial pixel // size of the fullscreen. dsf = 2.f; SetDeviceScaleFactor(dsf); { SCOPED_TRACE("Testing @ 2.0 device scale factor"); EXPECT_EQ(initial_root_window_size_pixels, test_delegate->GetCurrentVideoSize()); // The recording service still tracks the up-to-date DSF and frame sink // pixel size even though it doesn't change the video size from its initial // value. EXPECT_EQ( dsf, test_delegate->recording_service()->GetCurrentDeviceScaleFactor()); EXPECT_EQ(ToPixels(root->bounds().size(), dsf), test_delegate->GetCurrentFrameSinkSizeInPixels()); test_delegate->recording_service()->RequestAndWaitForVideoFrame( base::BindOnce(&CaptureModeRecordingSizeTest::VerifyVideoFrame, initial_root_window_size_pixels)); } // When recording the fullscreen, the video size never changes, and remains at // the initial pixel size of the recording. Hence, there should be no // reconfigures. EXPECT_EQ(0, test_delegate->recording_service() ->GetNumberOfVideoEncoderReconfigures()); } // The test is flaky. https://crbug.com/1287724. TEST_F(CaptureModeRecordingSizeTest, DISABLED_CaptureAtPixelsRegion) { float dsf = 1.6f; SetDeviceScaleFactor(dsf); auto* controller = StartVideoRecording(CaptureModeSource::kRegion); auto* test_delegate = static_cast(controller->delegate_for_testing()); ASSERT_TRUE(test_delegate->recording_service()); { SCOPED_TRACE("Testing @ 1.6 device scale factor"); const gfx::Size expected_video_size = ToPixels(user_region_.size(), dsf); EXPECT_EQ(expected_video_size, test_delegate->GetCurrentVideoSize()); EXPECT_EQ( dsf, test_delegate->recording_service()->GetCurrentDeviceScaleFactor()); test_delegate->recording_service()->RequestAndWaitForVideoFrame( base::BindOnce(&CaptureModeRecordingSizeTest::VerifyVideoFrame, expected_video_size)); } // Change the DSF and expect the video size to change to match the new pixel // size of the recorded target. dsf = 2.f; SetDeviceScaleFactor(dsf); { SCOPED_TRACE("Testing @ 2.0 device scale factor"); const gfx::Size expected_video_size = ToPixels(user_region_.size(), dsf); EXPECT_EQ(expected_video_size, test_delegate->GetCurrentVideoSize()); EXPECT_EQ( dsf, test_delegate->recording_service()->GetCurrentDeviceScaleFactor()); test_delegate->recording_service()->RequestAndWaitForVideoFrame( base::BindOnce(&CaptureModeRecordingSizeTest::VerifyVideoFrame, expected_video_size)); } // Since the user chooses the capture region in DIPs, its corresponding pixel // size will change when changing the device scale factor. Therefore, the // encoder is expected to reconfigure once. EXPECT_EQ(1, test_delegate->recording_service() ->GetNumberOfVideoEncoderReconfigures()); } // The test is flaky. https://crbug.com/1287724. TEST_F(CaptureModeRecordingSizeTest, DISABLED_CaptureAtPixelsWindow) { float dsf = 1.6f; SetDeviceScaleFactor(dsf); auto* controller = StartVideoRecording(CaptureModeSource::kWindow); auto* test_delegate = static_cast(controller->delegate_for_testing()); ASSERT_TRUE(test_delegate->recording_service()); { SCOPED_TRACE("Testing @ 1.6 device scale factor"); const gfx::Size expected_video_size = ToPixels(window_->GetBoundsInRootWindow().size(), dsf); EXPECT_EQ(expected_video_size, test_delegate->GetCurrentVideoSize()); EXPECT_EQ( dsf, test_delegate->recording_service()->GetCurrentDeviceScaleFactor()); test_delegate->recording_service()->RequestAndWaitForVideoFrame( base::BindOnce(&CaptureModeRecordingSizeTest::VerifyVideoFrame, expected_video_size)); } // Change the DSF and expect the video size to change to match the new pixel // size of the recorded target. dsf = 2.f; SetDeviceScaleFactor(dsf); { SCOPED_TRACE("Testing @ 2.0 device scale factor"); const gfx::Size expected_video_size = ToPixels(window_->GetBoundsInRootWindow().size(), dsf); EXPECT_EQ(expected_video_size, test_delegate->GetCurrentVideoSize()); EXPECT_EQ( dsf, test_delegate->recording_service()->GetCurrentDeviceScaleFactor()); test_delegate->recording_service()->RequestAndWaitForVideoFrame( base::BindOnce(&CaptureModeRecordingSizeTest::VerifyVideoFrame, expected_video_size)); } // When changing the device scale factor, the DIPs size of the window doesn't // change, but (like |kRegion|) its pixel size will. Hence, the // reconfiguration. EXPECT_EQ(1, test_delegate->recording_service() ->GetNumberOfVideoEncoderReconfigures()); } // Tests the behavior of screen recording with the presence of HDCP secure // content on the screen in all capture mode sources (fullscreen, region, and // window) depending on the test param. class CaptureModeHdcpTest : public CaptureModeTest, public ::testing::WithParamInterface { public: CaptureModeHdcpTest() = default; ~CaptureModeHdcpTest() override = default; // CaptureModeTest: void SetUp() override { CaptureModeTest::SetUp(); window_ = CreateTestWindow(gfx::Rect(200, 200)); // Create a child window with protected content. This simulates the real // behavior of a browser window hosting a page with protected content, where // the window that has a protection mask is the RenderWidgetHostViewAura, // which is a descendant of the BrowserFrame window which can get recorded. protected_content_window_ = CreateTestWindow(gfx::Rect(150, 150)); window_->AddChild(protected_content_window_.get()); protection_delegate_ = std::make_unique( protected_content_window_.get()); CaptureModeController::Get()->SetUserCaptureRegion(gfx::Rect(20, 50), /*by_user=*/true); } void TearDown() override { protection_delegate_.reset(); protected_content_window_.reset(); window_.reset(); CaptureModeTest::TearDown(); } // Enters the capture mode session. void StartSessionForVideo() { StartCaptureSession(GetParam(), CaptureModeType::kVideo); } // Attempts video recording from the capture mode source set by the test // param. void AttemptRecording() { auto* controller = CaptureModeController::Get(); ASSERT_TRUE(controller->IsActive()); switch (GetParam()) { case CaptureModeSource::kFullscreen: case CaptureModeSource::kRegion: controller->StartVideoRecordingImmediatelyForTesting(); break; case CaptureModeSource::kWindow: // Window capture mode selects the window under the cursor as the // capture source. auto* event_generator = GetEventGenerator(); event_generator->MoveMouseToCenterOf(window_.get()); controller->StartVideoRecordingImmediatelyForTesting(); break; } } protected: std::unique_ptr window_; std::unique_ptr protected_content_window_; std::unique_ptr protection_delegate_; }; TEST_P(CaptureModeHdcpTest, WindowBecomesProtectedWhileRecording) { StartSessionForVideo(); AttemptRecording(); WaitForRecordingToStart(); auto* controller = CaptureModeController::Get(); EXPECT_TRUE(controller->is_recording_in_progress()); // The window becomes HDCP protected, which should end video recording. base::HistogramTester histogram_tester; protection_delegate_->SetProtection(display::CONTENT_PROTECTION_METHOD_HDCP, base::DoNothing()); EXPECT_FALSE(controller->is_recording_in_progress()); histogram_tester.ExpectBucketCount( kEndRecordingReasonInClamshellHistogramName, EndRecordingReason::kHdcpInterruption, 1); } TEST_P(CaptureModeHdcpTest, ProtectedWindowDestruction) { auto window_2 = CreateTestWindow(gfx::Rect(100, 50)); OutputProtectionDelegate protection_delegate_2(window_2.get()); protection_delegate_2.SetProtection(display::CONTENT_PROTECTION_METHOD_HDCP, base::DoNothing()); StartSessionForVideo(); AttemptRecording(); // Recording cannot start because of another protected window on the screen, // except when we're capturing a different |window_|. auto* controller = CaptureModeController::Get(); EXPECT_FALSE(controller->IsActive()); if (GetParam() == CaptureModeSource::kWindow) { WaitForRecordingToStart(); EXPECT_TRUE(controller->is_recording_in_progress()); controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton); EXPECT_FALSE(controller->is_recording_in_progress()); // Wait for the video file to be saved so that we can start a new recording. WaitForCaptureFileToBeSaved(); } else { EXPECT_FALSE(controller->is_recording_in_progress()); } // When the protected window is destroyed, it's possbile now to record from // all capture sources. window_2.reset(); StartSessionForVideo(); AttemptRecording(); WaitForRecordingToStart(); EXPECT_FALSE(controller->IsActive()); EXPECT_TRUE(controller->is_recording_in_progress()); } TEST_P(CaptureModeHdcpTest, WindowBecomesProtectedBeforeRecording) { protection_delegate_->SetProtection(display::CONTENT_PROTECTION_METHOD_HDCP, base::DoNothing()); StartSessionForVideo(); AttemptRecording(); // Recording cannot even start. auto* controller = CaptureModeController::Get(); EXPECT_FALSE(controller->is_recording_in_progress()); EXPECT_FALSE(controller->IsActive()); } TEST_P(CaptureModeHdcpTest, ProtectedWindowInMultiDisplay) { UpdateDisplay("500x400,401+0-500x400"); auto roots = Shell::GetAllRootWindows(); ASSERT_EQ(2u, roots.size()); protection_delegate_->SetProtection(display::CONTENT_PROTECTION_METHOD_HDCP, base::DoNothing()); // Move the cursor to the secondary display before starting the session to // make sure the session starts on that display. auto* event_generator = GetEventGenerator(); MoveMouseToAndUpdateCursorDisplay(roots[1]->GetBoundsInScreen().CenterPoint(), event_generator); StartSessionForVideo(); // Also, make sure the selected region is in the secondary display. auto* controller = CaptureModeController::Get(); EXPECT_EQ(controller->capture_mode_session()->current_root(), roots[1]); AttemptRecording(); // Recording should be able to start (since the protected window is on the // first display) unless the protected window itself is the one being // recorded. if (GetParam() == CaptureModeSource::kWindow) { EXPECT_FALSE(controller->is_recording_in_progress()); } else { WaitForRecordingToStart(); EXPECT_TRUE(controller->is_recording_in_progress()); // Moving the protected window to the display being recorded should // terminate the recording. base::HistogramTester histogram_tester; window_util::MoveWindowToDisplay(window_.get(), roots[1]->GetHost()->GetDisplayId()); ASSERT_EQ(window_->GetRootWindow(), roots[1]); ASSERT_EQ(protected_content_window_->GetRootWindow(), roots[1]); EXPECT_FALSE(controller->is_recording_in_progress()); histogram_tester.ExpectBucketCount( kEndRecordingReasonInClamshellHistogramName, EndRecordingReason::kHdcpInterruption, 1); } } INSTANTIATE_TEST_SUITE_P(All, CaptureModeHdcpTest, testing::Values(CaptureModeSource::kFullscreen, CaptureModeSource::kRegion, CaptureModeSource::kWindow)); TEST_F(CaptureModeTest, ClosingWindowBeingRecorded) { auto window = CreateTestWindow(gfx::Rect(200, 200)); StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo); auto* event_generator = GetEventGenerator(); event_generator->MoveMouseToCenterOf(window.get()); auto* controller = CaptureModeController::Get(); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); // The window should have a valid capture ID. EXPECT_TRUE(window->subtree_capture_id().is_valid()); // Generate a couple of mouse moves, so that the second one gets throttled // using the `VideoRecordingWatcher::cursor_events_throttle_timer_`. This is // needed for a regression testing of https://crbug.com/1273609. event_generator->MoveMouseBy(20, 30); event_generator->MoveMouseBy(-10, -20); // Closing the window being recorded should end video recording. base::HistogramTester histogram_tester; window.reset(); auto* stop_recording_button = Shell::GetPrimaryRootWindowController() ->GetStatusAreaWidget() ->stop_recording_button_tray(); EXPECT_FALSE(stop_recording_button->visible_preferred()); EXPECT_FALSE(controller->is_recording_in_progress()); WaitForCaptureFileToBeSaved(); EXPECT_FALSE(controller->video_recording_watcher_for_testing()); histogram_tester.ExpectBucketCount( kEndRecordingReasonInClamshellHistogramName, EndRecordingReason::kDisplayOrWindowClosing, 1); } TEST_F(CaptureModeTest, DetachDisplayWhileWindowRecording) { UpdateDisplay("500x400,401+0-500x400"); // Create a window on the second display. auto window = CreateTestWindow(gfx::Rect(450, 20, 200, 200)); auto roots = Shell::GetAllRootWindows(); ASSERT_EQ(2u, roots.size()); EXPECT_EQ(window->GetRootWindow(), roots[1]); StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kVideo); auto* event_generator = GetEventGenerator(); MoveMouseToAndUpdateCursorDisplay(window->GetBoundsInScreen().CenterPoint(), event_generator); auto* controller = CaptureModeController::Get(); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); auto* stop_recording_button = RootWindowController::ForWindow(roots[1]) ->GetStatusAreaWidget() ->stop_recording_button_tray(); EXPECT_TRUE(stop_recording_button->visible_preferred()); // Disconnecting the display, on which the window being recorded is located, // should not end the recording. The window should be reparented to another // display, and the stop-recording button should move with to that display. RemoveSecondaryDisplay(); roots = Shell::GetAllRootWindows(); ASSERT_EQ(1u, roots.size()); EXPECT_TRUE(controller->is_recording_in_progress()); stop_recording_button = RootWindowController::ForWindow(roots[0]) ->GetStatusAreaWidget() ->stop_recording_button_tray(); EXPECT_TRUE(stop_recording_button->visible_preferred()); } TEST_F(CaptureModeTest, SuspendWhileSessionIsActive) { auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); EXPECT_TRUE(controller->IsActive()); power_manager_client()->SendSuspendImminent( power_manager::SuspendImminent::IDLE); EXPECT_FALSE(controller->IsActive()); } TEST_F(CaptureModeTest, SuspendAfterCountdownStarts) { // User NORMAL_DURATION for the countdown animation so we can have predictable // timings. ui::ScopedAnimationDurationScaleMode animation_scale( ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION); auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); // Hit Enter to begin recording, wait for 1 second, then suspend the device. auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_RETURN, event_generator); WaitForSeconds(1); power_manager_client()->SendSuspendImminent( power_manager::SuspendImminent::IDLE); EXPECT_FALSE(controller->IsActive()); EXPECT_FALSE(controller->is_recording_in_progress()); } TEST_F(CaptureModeTest, SuspendAfterRecordingStarts) { auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); base::HistogramTester histogram_tester; power_manager_client()->SendSuspendImminent( power_manager::SuspendImminent::IDLE); EXPECT_FALSE(controller->is_recording_in_progress()); histogram_tester.ExpectBucketCount( kEndRecordingReasonInClamshellHistogramName, EndRecordingReason::kImminentSuspend, 1); } TEST_F(CaptureModeTest, SwitchUsersWhileRecording) { auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); StartVideoRecordingImmediately(); base::HistogramTester histogram_tester; EXPECT_TRUE(controller->is_recording_in_progress()); SwitchToUser2(); EXPECT_FALSE(controller->is_recording_in_progress()); histogram_tester.ExpectBucketCount( kEndRecordingReasonInClamshellHistogramName, EndRecordingReason::kActiveUserChange, 1); } TEST_F(CaptureModeTest, SwitchUsersAfterCountdownStarts) { // User NORMAL_DURATION for the countdown animation so we can have predictable // timings. ui::ScopedAnimationDurationScaleMode animation_scale( ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION); auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); // Hit Enter to begin recording, wait for 1 second, then switch users. auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_RETURN, event_generator); WaitForSeconds(1); SwitchToUser2(); EXPECT_FALSE(controller->IsActive()); EXPECT_FALSE(controller->is_recording_in_progress()); } TEST_F(CaptureModeTest, ClosingDisplayBeingFullscreenRecorded) { UpdateDisplay("500x400,401+0-500x400"); auto roots = Shell::GetAllRootWindows(); ASSERT_EQ(2u, roots.size()); StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); auto* event_generator = GetEventGenerator(); MoveMouseToAndUpdateCursorDisplay(roots[1]->GetBoundsInScreen().CenterPoint(), event_generator); auto* controller = CaptureModeController::Get(); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); auto* stop_recording_button = RootWindowController::ForWindow(roots[1]) ->GetStatusAreaWidget() ->stop_recording_button_tray(); EXPECT_TRUE(stop_recording_button->visible_preferred()); // Disconnecting the display being fullscreen recorded should end the // recording and remove the stop recording button. base::HistogramTester histogram_tester; RemoveSecondaryDisplay(); roots = Shell::GetAllRootWindows(); ASSERT_EQ(1u, roots.size()); EXPECT_FALSE(controller->is_recording_in_progress()); stop_recording_button = RootWindowController::ForWindow(roots[0]) ->GetStatusAreaWidget() ->stop_recording_button_tray(); EXPECT_FALSE(stop_recording_button->visible_preferred()); histogram_tester.ExpectBucketCount( kEndRecordingReasonInClamshellHistogramName, EndRecordingReason::kDisplayOrWindowClosing, 1); } TEST_F(CaptureModeTest, ShuttingDownWhileRecording) { StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); auto* controller = CaptureModeController::Get(); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); // Exiting the test now will shut down ash while recording is in progress, // there should be no crashes when // VideoRecordingWatcher::OnChromeTerminating() terminates the recording. } // Tests that metrics are recorded properly for capture mode bar buttons. TEST_F(CaptureModeTest, CaptureModeBarButtonTypeHistograms) { constexpr char kClamshellHistogram[] = "Ash.CaptureModeController.BarButtons.ClamshellMode"; constexpr char kTabletHistogram[] = "Ash.CaptureModeController.BarButtons.TabletMode"; base::HistogramTester histogram_tester; CaptureModeController::Get()->Start(CaptureModeEntryType::kQuickSettings); auto* event_generator = GetEventGenerator(); // Tests each bar button in clamshell mode. ClickOnView(GetImageToggleButton(), event_generator); histogram_tester.ExpectBucketCount( kClamshellHistogram, CaptureModeBarButtonType::kScreenCapture, 1); ClickOnView(GetVideoToggleButton(), event_generator); histogram_tester.ExpectBucketCount( kClamshellHistogram, CaptureModeBarButtonType::kScreenRecord, 1); ClickOnView(GetFullscreenToggleButton(), event_generator); histogram_tester.ExpectBucketCount(kClamshellHistogram, CaptureModeBarButtonType::kFull, 1); ClickOnView(GetRegionToggleButton(), event_generator); histogram_tester.ExpectBucketCount(kClamshellHistogram, CaptureModeBarButtonType::kRegion, 1); ClickOnView(GetWindowToggleButton(), event_generator); histogram_tester.ExpectBucketCount(kClamshellHistogram, CaptureModeBarButtonType::kWindow, 1); // Enter tablet mode and test the bar buttons. auto* tablet_mode_controller = Shell::Get()->tablet_mode_controller(); tablet_mode_controller->SetEnabledForTest(true); ASSERT_TRUE(tablet_mode_controller->InTabletMode()); ClickOnView(GetImageToggleButton(), event_generator); histogram_tester.ExpectBucketCount( kTabletHistogram, CaptureModeBarButtonType::kScreenCapture, 1); ClickOnView(GetVideoToggleButton(), event_generator); histogram_tester.ExpectBucketCount( kTabletHistogram, CaptureModeBarButtonType::kScreenRecord, 1); ClickOnView(GetFullscreenToggleButton(), event_generator); histogram_tester.ExpectBucketCount(kTabletHistogram, CaptureModeBarButtonType::kFull, 1); ClickOnView(GetRegionToggleButton(), event_generator); histogram_tester.ExpectBucketCount(kTabletHistogram, CaptureModeBarButtonType::kRegion, 1); ClickOnView(GetWindowToggleButton(), event_generator); histogram_tester.ExpectBucketCount(kTabletHistogram, CaptureModeBarButtonType::kWindow, 1); } TEST_F(CaptureModeTest, CaptureSessionSwitchedModeMetric) { constexpr char kHistogramName[] = "Ash.CaptureModeController.SwitchesFromInitialCaptureMode"; base::HistogramTester histogram_tester; histogram_tester.ExpectBucketCount(kHistogramName, false, 0); histogram_tester.ExpectBucketCount(kHistogramName, true, 0); // Perform a capture without switching modes. A false should be recorded. auto* controller = StartImageRegionCapture(); SelectRegion(gfx::Rect(100, 100)); auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_RETURN, event_generator); histogram_tester.ExpectBucketCount(kHistogramName, false, 1); histogram_tester.ExpectBucketCount(kHistogramName, true, 0); // Perform a capture after switching to fullscreen mode. A true should be // recorded. controller->Start(CaptureModeEntryType::kQuickSettings); ClickOnView(GetFullscreenToggleButton(), event_generator); SendKey(ui::VKEY_RETURN, event_generator); histogram_tester.ExpectBucketCount(kHistogramName, false, 1); histogram_tester.ExpectBucketCount(kHistogramName, true, 1); // Perform a capture after switching to another mode and back to the original // mode. A true should still be recorded as there was some switching done. controller->Start(CaptureModeEntryType::kQuickSettings); ClickOnView(GetRegionToggleButton(), event_generator); ClickOnView(GetFullscreenToggleButton(), event_generator); SendKey(ui::VKEY_RETURN, event_generator); histogram_tester.ExpectBucketCount(kHistogramName, false, 1); histogram_tester.ExpectBucketCount(kHistogramName, true, 2); } // Test that cancel recording during countdown won't cause crash. TEST_F(CaptureModeTest, CancelCaptureDuringCountDown) { ui::ScopedAnimationDurationScaleMode animation_scale( ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION); StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); // Hit Enter to begin recording, Wait for 1 second, then press ESC while count // down is in progress. auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_RETURN, event_generator); WaitForSeconds(1); CaptureModeTestApi test_api; EXPECT_TRUE(test_api.IsInCountDownAnimation()); SendKey(ui::VKEY_ESCAPE, event_generator); EXPECT_FALSE(test_api.IsInCountDownAnimation()); EXPECT_FALSE(test_api.IsSessionActive()); EXPECT_FALSE(test_api.IsVideoRecordingInProgress()); } TEST_F(CaptureModeTest, EscDuringCountDownWhileSettingsOpen) { ui::ScopedAnimationDurationScaleMode animation_scale( ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION); StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); // Hitting Esc while the settings menu is open and the count down is in // progress should end the session directly. auto* event_generator = GetEventGenerator(); ClickOnView(GetSettingsButton(), event_generator); EXPECT_TRUE(GetCaptureModeSettingsWidget()); SendKey(ui::VKEY_RETURN, event_generator); WaitForSeconds(1); CaptureModeTestApi test_api; EXPECT_TRUE(test_api.IsInCountDownAnimation()); SendKey(ui::VKEY_ESCAPE, event_generator); EXPECT_FALSE(test_api.IsInCountDownAnimation()); EXPECT_FALSE(test_api.IsSessionActive()); EXPECT_FALSE(test_api.IsVideoRecordingInProgress()); } // Tests that metrics are recorded properly for capture region adjustments. TEST_F(CaptureModeTest, NumberOfCaptureRegionAdjustmentsHistogram) { constexpr char kClamshellHistogram[] = "Ash.CaptureModeController.CaptureRegionAdjusted.ClamshellMode"; constexpr char kTabletHistogram[] = "Ash.CaptureModeController.CaptureRegionAdjusted.TabletMode"; base::HistogramTester histogram_tester; UpdateDisplay("800x700"); auto* controller = StartImageRegionCapture(); // Create the initial region. const gfx::Rect target_region(gfx::Rect(200, 200, 400, 400)); SelectRegion(target_region); auto resize_and_reset_region = [](ui::test::EventGenerator* event_generator, const gfx::Point& top_right) { // Enlarges the region and then resize it back to its original size. event_generator->set_current_screen_location(top_right); event_generator->DragMouseTo(top_right + gfx::Vector2d(50, 50)); event_generator->DragMouseTo(top_right); }; auto move_and_reset_region = [](ui::test::EventGenerator* event_generator, const gfx::Point& drag_point) { // Moves the region and then moves it back to its original position. event_generator->set_current_screen_location(drag_point); event_generator->DragMouseTo(drag_point + gfx::Vector2d(-50, -50)); event_generator->DragMouseTo(drag_point); }; // Resize the region twice by dragging the top right of the region out and // then back again. auto* event_generator = GetEventGenerator(); auto top_right = target_region.top_right(); resize_and_reset_region(event_generator, top_right); // Move the region twice by dragging within the region. const gfx::Point drag_point(300, 300); move_and_reset_region(event_generator, drag_point); // Perform a capture to record the count. controller->PerformCapture(); histogram_tester.ExpectBucketCount(kClamshellHistogram, 4, 1); // Create a new image region capture. Move the region twice then change // sources to fullscreen and back to region. This toggle should reset the // count. Perform a capture to record the count. StartImageRegionCapture(); move_and_reset_region(event_generator, drag_point); controller->SetSource(CaptureModeSource::kFullscreen); controller->SetSource(CaptureModeSource::kRegion); controller->PerformCapture(); histogram_tester.ExpectBucketCount(kClamshellHistogram, 0, 1); // Enter tablet mode and restart the capture session. The capture region // should be remembered. auto* tablet_mode_controller = Shell::Get()->tablet_mode_controller(); tablet_mode_controller->SetEnabledForTest(true); ASSERT_TRUE(tablet_mode_controller->InTabletMode()); StartImageRegionCapture(); ASSERT_EQ(target_region, controller->user_capture_region()); // Resize the region twice by dragging the top right of the region out and // then back again. resize_and_reset_region(event_generator, top_right); // Move the region twice by dragging within the region. move_and_reset_region(event_generator, drag_point); // Perform a capture to record the count. controller->PerformCapture(); histogram_tester.ExpectBucketCount(kTabletHistogram, 4, 1); // Restart the region capture and resize it. Then create a new region by // dragging outside of the existing capture region. This should reset the // counter. Change source to record a sample. StartImageRegionCapture(); resize_and_reset_region(event_generator, top_right); SelectRegion(gfx::Rect(0, 0, 100, 100)); controller->PerformCapture(); histogram_tester.ExpectBucketCount(kTabletHistogram, 0, 1); } TEST_F(CaptureModeTest, FullscreenCapture) { ui::ScopedAnimationDurationScaleMode animation_scale( ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION); CaptureModeController* controller = StartCaptureSession( CaptureModeSource::kFullscreen, CaptureModeType::kImage); EXPECT_TRUE(controller->IsActive()); // Press anywhere to capture image. auto* event_generator = GetEventGenerator(); event_generator->ClickLeftButton(); EXPECT_FALSE(controller->IsActive()); controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); EXPECT_TRUE(controller->IsActive()); // Press anywhere to capture video. event_generator->ClickLeftButton(); WaitForRecordingToStart(); EXPECT_FALSE(controller->IsActive()); } // Tests that metrics are recorded properly for capture mode configurations when // taking a screenshot. TEST_F(CaptureModeTest, ScreenshotConfigurationHistogram) { constexpr char kClamshellHistogram[] = "Ash.CaptureModeController.CaptureConfiguration.ClamshellMode"; constexpr char kTabletHistogram[] = "Ash.CaptureModeController.CaptureConfiguration.TabletMode"; base::HistogramTester histogram_tester; // Use a set display size as we will be choosing points in this test. UpdateDisplay("800x700"); // Create a window for window captures later. std::unique_ptr window1( CreateTestWindow(gfx::Rect(600, 600, 100, 100))); // Perform a fullscreen screenshot. auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); controller->PerformCapture(); histogram_tester.ExpectBucketCount( kClamshellHistogram, CaptureModeConfiguration::kFullscreenScreenshot, 1); // Perform a region screenshot. controller = StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kImage); const gfx::Rect capture_region(200, 200, 400, 400); SelectRegion(capture_region); controller->PerformCapture(); histogram_tester.ExpectBucketCount( kClamshellHistogram, CaptureModeConfiguration::kRegionScreenshot, 1); // Perform a window screenshot. controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kImage); auto* event_generator = GetEventGenerator(); event_generator->MoveMouseToCenterOf(window1.get()); EXPECT_EQ(window1.get(), controller->capture_mode_session()->GetSelectedWindow()); controller->PerformCapture(); histogram_tester.ExpectBucketCount( kClamshellHistogram, CaptureModeConfiguration::kWindowScreenshot, 1); // Switch to tablet mode. auto* tablet_mode_controller = Shell::Get()->tablet_mode_controller(); tablet_mode_controller->SetEnabledForTest(true); ASSERT_TRUE(tablet_mode_controller->InTabletMode()); // Perform a fullscreen screenshot. controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); controller->PerformCapture(); histogram_tester.ExpectBucketCount( kTabletHistogram, CaptureModeConfiguration::kFullscreenScreenshot, 1); // Perform a region screenshot. controller = StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kImage); controller->PerformCapture(); histogram_tester.ExpectBucketCount( kTabletHistogram, CaptureModeConfiguration::kRegionScreenshot, 1); // Perform a window screenshot. controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kImage); event_generator->MoveMouseToCenterOf(window1.get()); EXPECT_EQ(window1.get(), controller->capture_mode_session()->GetSelectedWindow()); controller->PerformCapture(); histogram_tester.ExpectBucketCount( kTabletHistogram, CaptureModeConfiguration::kWindowScreenshot, 1); } // Tests that there is no crash when touching the capture label widget in tablet // mode when capturing a window. Regression test for https://crbug.com/1152938. TEST_F(CaptureModeTest, TabletTouchCaptureLabelWidgetWindowMode) { SwitchToTabletMode(); // Enter capture window mode. CaptureModeController* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kImage); ASSERT_TRUE(controller->IsActive()); // Press and release on where the capture label widget would be. auto* event_generator = GetEventGenerator(); DCHECK(GetCaptureModeLabelWidget()); event_generator->set_current_screen_location( GetCaptureModeLabelWidget()->GetWindowBoundsInScreen().CenterPoint()); event_generator->PressTouch(); event_generator->ReleaseTouch(); // There are no windows and home screen window is excluded from window capture // mode, so capture mode will still remain active. EXPECT_TRUE(Shell::Get()->app_list_controller()->IsHomeScreenVisible()); EXPECT_TRUE(controller->IsActive()); } // Tests that after rotating a display, the capture session widgets are updated // and the capture region is reset. TEST_F(CaptureModeTest, DisplayRotation) { UpdateDisplay("1200x600"); auto* controller = StartImageRegionCapture(); SelectRegion(gfx::Rect(1200, 400)); OpenSettingsView(); // Rotate the primary display by 90 degrees. Test that the region, capture // bar and capture settings fit within the rotated bounds, and the capture // label widget is still centered in the region. Shell::Get()->display_manager()->SetDisplayRotation( WindowTreeHostManager::GetPrimaryDisplayId(), display::Display::ROTATE_90, display::Display::RotationSource::USER); const gfx::Rect rotated_root_bounds(600, 1200); EXPECT_TRUE(rotated_root_bounds.Contains(controller->user_capture_region())); const gfx::Rect capture_bar_bounds = GetCaptureModeBarView()->GetBoundsInScreen(); const gfx::Rect settings_bounds = CaptureModeSettingsTestApi().GetSettingsView()->GetBoundsInScreen(); EXPECT_TRUE(rotated_root_bounds.Contains(capture_bar_bounds)); EXPECT_TRUE(rotated_root_bounds.Contains(settings_bounds)); // Verify that the space between the bottom of the settings and the top // of the capture bar is `kSpaceBetweenCaptureBarAndSettingsMenu`. EXPECT_EQ(capture_bar_bounds.y() - settings_bounds.bottom(), capture_mode::kSpaceBetweenCaptureBarAndSettingsMenu); views::Widget* capture_label_widget = GetCaptureModeLabelWidget(); ASSERT_TRUE(capture_label_widget); EXPECT_EQ(controller->user_capture_region().CenterPoint(), capture_label_widget->GetWindowBoundsInScreen().CenterPoint()); } TEST_F(CaptureModeTest, DisplayBoundsChange) { UpdateDisplay("1200x600"); auto* controller = StartImageRegionCapture(); SelectRegion(gfx::Rect(1200, 400)); // Shrink the display. The capture region should shrink, and the capture bar // should be adjusted to be centered. UpdateDisplay("700x600"); EXPECT_EQ(gfx::Rect(700, 400), controller->user_capture_region()); EXPECT_EQ(350, GetCaptureModeBarView()->GetBoundsInScreen().CenterPoint().x()); } TEST_F(CaptureModeTest, ReenterOnSmallerDisplay) { UpdateDisplay("1200x600,1201+0-700x600"); // Start off with the primary display as the targeted display. Create a region // that fits the primary display but would be too big for the secondary // display. auto* event_generator = GetEventGenerator(); MoveMouseToAndUpdateCursorDisplay(gfx::Point(700, 300), event_generator); auto* controller = StartImageRegionCapture(); SelectRegion(gfx::Rect(1200, 400)); EXPECT_EQ(gfx::Rect(1200, 400), controller->user_capture_region()); controller->Stop(); // Make the secondary display the targeted display. Test that the region has // shrunk to fit the display. MoveMouseToAndUpdateCursorDisplay(gfx::Point(1500, 300), event_generator); StartImageRegionCapture(); EXPECT_EQ(gfx::Rect(700, 400), controller->user_capture_region()); } // Tests tabbing when in capture window mode. TEST_F(CaptureModeTest, KeyboardNavigationBasic) { auto* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kImage); using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; CaptureModeSessionTestApi test_api(controller->capture_mode_session()); // Initially nothing is focused. EXPECT_EQ(FocusGroup::kNone, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); // Tab once, we are now focusing the type and source buttons group on the // capture bar. auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kTypeSource, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); // Tab four times to focus the last source button (window mode button). SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/4); EXPECT_EQ(FocusGroup::kTypeSource, test_api.GetCurrentFocusGroup()); EXPECT_EQ(4u, test_api.GetCurrentFocusIndex()); // Tab once to focus the settings and close buttons group on the capture bar. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); // Shift tab to focus the last source button again. SendKey(ui::VKEY_TAB, event_generator, ui::EF_SHIFT_DOWN); EXPECT_EQ(FocusGroup::kTypeSource, test_api.GetCurrentFocusGroup()); EXPECT_EQ(4u, test_api.GetCurrentFocusIndex()); // Press esc to clear focus, but remain in capture mode. SendKey(ui::VKEY_ESCAPE, event_generator); EXPECT_EQ(FocusGroup::kNone, test_api.GetCurrentFocusGroup()); EXPECT_TRUE(controller->IsActive()); // Tests that pressing esc when there is no focus will exit capture mode. SendKey(ui::VKEY_ESCAPE, event_generator); EXPECT_FALSE(controller->IsActive()); } // Tests tabbing through windows on multiple displays when in capture window // mode. TEST_F(CaptureModeTest, KeyboardNavigationTabThroughWindowsOnMultipleDisplays) { UpdateDisplay("800x700,801+0-800x700"); std::vector root_windows = Shell::GetAllRootWindows(); ASSERT_EQ(2u, root_windows.size()); // Create three windows, one of them is a modal transient child. std::unique_ptr window1( CreateTestWindow(gfx::Rect(0, 0, 200, 200))); auto window1_transient = CreateTransientModalChildWindow( gfx::Rect(20, 30, 200, 150), window1.get()); std::unique_ptr window2( CreateTestWindow(gfx::Rect(900, 0, 200, 200))); auto* controller = StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kImage); CaptureModeSession* capture_mode_session = controller->capture_mode_session(); using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; CaptureModeSessionTestApi test_api(capture_mode_session); // Initially nothing is focused. EXPECT_EQ(FocusGroup::kNone, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); // Tab five times, we are now focusing the window mode button on the // capture bar. auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/5); EXPECT_EQ(FocusGroup::kTypeSource, test_api.GetCurrentFocusGroup()); EXPECT_EQ(4u, test_api.GetCurrentFocusIndex()); // Enter space to select window mode. SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(FocusGroup::kTypeSource, test_api.GetCurrentFocusGroup()); EXPECT_EQ(CaptureModeSource::kWindow, controller->source()); // Tab once, we are now focusing |window2| and capture mode bar is on // display2. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kCaptureWindow, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); EXPECT_EQ(window2.get(), capture_mode_session->GetSelectedWindow()); EXPECT_EQ(root_windows[1], capture_mode_session->current_root()); // Tab once, we are now focusing |window1_transient|. Since // |window1_transient| is on display1, capture mode bar will be moved to // display1 as well. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kCaptureWindow, test_api.GetCurrentFocusGroup()); EXPECT_EQ(1u, test_api.GetCurrentFocusIndex()); EXPECT_EQ(window1_transient.get(), capture_mode_session->GetSelectedWindow()); EXPECT_EQ(root_windows[0], capture_mode_session->current_root()); // Tab once, we are now focusing |window1|. Capture mode bar still stays on // display1. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kCaptureWindow, test_api.GetCurrentFocusGroup()); EXPECT_EQ(2u, test_api.GetCurrentFocusIndex()); EXPECT_EQ(window1.get(), capture_mode_session->GetSelectedWindow()); EXPECT_EQ(root_windows[0], capture_mode_session->current_root()); // Press space, make sure nothing is changed and no crash. SendKey(ui::VKEY_SPACE, event_generator); EXPECT_TRUE(controller->IsActive()); EXPECT_EQ(window1.get(), capture_mode_session->GetSelectedWindow()); EXPECT_EQ(root_windows[0], capture_mode_session->current_root()); // Tab once to focus the settings and close buttons group on the capture bar. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); // Shift tab to focus |window1| again. SendKey(ui::VKEY_TAB, event_generator, ui::EF_SHIFT_DOWN); EXPECT_EQ(FocusGroup::kCaptureWindow, test_api.GetCurrentFocusGroup()); EXPECT_EQ(2u, test_api.GetCurrentFocusIndex()); EXPECT_EQ(window1.get(), capture_mode_session->GetSelectedWindow()); // Shift tab to focus |window1_transient|. SendKey(ui::VKEY_TAB, event_generator, ui::EF_SHIFT_DOWN); EXPECT_EQ(FocusGroup::kCaptureWindow, test_api.GetCurrentFocusGroup()); EXPECT_EQ(1u, test_api.GetCurrentFocusIndex()); EXPECT_EQ(window1_transient.get(), capture_mode_session->GetSelectedWindow()); EXPECT_EQ(root_windows[0], capture_mode_session->current_root()); // Shift tab to focus |window2|. Capture mode bar will be moved to display2 as // well. SendKey(ui::VKEY_TAB, event_generator, ui::EF_SHIFT_DOWN); EXPECT_EQ(FocusGroup::kCaptureWindow, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); EXPECT_EQ(window2.get(), capture_mode_session->GetSelectedWindow()); EXPECT_EQ(root_windows[1], capture_mode_session->current_root()); // Press esc to clear focus, but remain in capture mode with |window2| // selected. SendKey(ui::VKEY_ESCAPE, event_generator); EXPECT_EQ(FocusGroup::kNone, test_api.GetCurrentFocusGroup()); EXPECT_TRUE(controller->IsActive()); EXPECT_EQ(window2.get(), capture_mode_session->GetSelectedWindow()); EXPECT_EQ(root_windows[1], capture_mode_session->current_root()); // Press return. Since there's a selected window, capture mode will // be ended after capturing the selected window. SendKey(ui::VKEY_RETURN, event_generator); EXPECT_FALSE(controller->IsActive()); } // Tests that a click will remove focus. TEST_F(CaptureModeTest, KeyboardNavigationClicksRemoveFocus) { auto* controller = StartImageRegionCapture(); auto* event_generator = GetEventGenerator(); CaptureModeSessionTestApi test_api(controller->capture_mode_session()); SendKey(ui::VKEY_TAB, event_generator); EXPECT_TRUE(test_api.HasFocus()); event_generator->ClickLeftButton(); EXPECT_FALSE(test_api.HasFocus()); } // Tests that pressing space on a focused button will click it. TEST_F(CaptureModeTest, KeyboardNavigationSpaceToClickButtons) { auto* controller = StartImageRegionCapture(); SelectRegion(gfx::Rect(200, 200)); auto* event_generator = GetEventGenerator(); // Tab to the button which changes the capture type to video and hit space. SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/2); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(CaptureModeType::kVideo, controller->type()); // Shift tab and space to change the capture type back to image. SendKey(ui::VKEY_TAB, event_generator, ui::EF_SHIFT_DOWN); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(CaptureModeType::kImage, controller->type()); // Tab to the fullscreen button and hit space. SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/2); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(CaptureModeSource::kFullscreen, controller->source()); // Tab to the region button and hit space to return to region capture mode. SendKey(ui::VKEY_TAB, event_generator); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(CaptureModeSource::kRegion, controller->source()); // Tab to the capture button and hit space to perform a capture, which exits // capture mode. SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/11); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_FALSE(controller->IsActive()); } // Tests that functionality to create and adjust a region with keyboard // shortcuts works as intended. TEST_F(CaptureModeTest, KeyboardNavigationSelectRegion) { auto* controller = StartImageRegionCapture(); auto* event_generator = GetEventGenerator(); ASSERT_TRUE(controller->user_capture_region().IsEmpty()); // Test that hitting space will create a default region. SendKey(ui::VKEY_SPACE, event_generator); gfx::Rect capture_region = controller->user_capture_region(); EXPECT_FALSE(capture_region.IsEmpty()); // Test that hitting an arrow key will do nothing as the selection region is // not focused initially. SendKey(ui::VKEY_RIGHT, event_generator); EXPECT_EQ(capture_region, controller->user_capture_region()); const int arrow_shift = capture_mode::kArrowKeyboardRegionChangeDp; // Hit tab until the whole region is focused. SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/6); CaptureModeSessionTestApi test_api(controller->capture_mode_session()); EXPECT_EQ(CaptureModeSessionFocusCycler::FocusGroup::kSelection, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); // Arrow keys should shift the whole region. SendKey(ui::VKEY_RIGHT, event_generator); EXPECT_EQ(capture_region.origin() + gfx::Vector2d(arrow_shift, 0), controller->user_capture_region().origin()); EXPECT_EQ(capture_region.size(), controller->user_capture_region().size()); SendKey(ui::VKEY_RIGHT, event_generator, ui::EF_SHIFT_DOWN); EXPECT_EQ( capture_region.origin() + gfx::Vector2d( arrow_shift + capture_mode::kShiftArrowKeyboardRegionChangeDp, 0), controller->user_capture_region().origin()); EXPECT_EQ(capture_region.size(), controller->user_capture_region().size()); // Hit tab so that the top left affordance circle is focused. Left and up keys // should enlarge the region, right and bottom keys should shrink the region. capture_region = controller->user_capture_region(); SendKey(ui::VKEY_TAB, event_generator); SendKey(ui::VKEY_LEFT, event_generator); SendKey(ui::VKEY_UP, event_generator); EXPECT_EQ(capture_region.size() + gfx::Size(arrow_shift, arrow_shift), controller->user_capture_region().size()); SendKey(ui::VKEY_RIGHT, event_generator); SendKey(ui::VKEY_DOWN, event_generator); EXPECT_EQ(capture_region.size(), controller->user_capture_region().size()); // Tab until we focus the bottom right affordance circle. Left and up keys // should shrink the region, right and bottom keys should enlarge the region. SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/4); SendKey(ui::VKEY_LEFT, event_generator); SendKey(ui::VKEY_UP, event_generator); EXPECT_EQ(capture_region.size() - gfx::Size(arrow_shift, arrow_shift), controller->user_capture_region().size()); SendKey(ui::VKEY_RIGHT, event_generator); SendKey(ui::VKEY_DOWN, event_generator); EXPECT_EQ(capture_region.size(), controller->user_capture_region().size()); } // Tests behavior regarding the default region when using keyboard navigation. TEST_F(CaptureModeTest, KeyboardNavigationDefaultRegion) { auto* controller = StartImageRegionCapture(); auto* event_generator = GetEventGenerator(); ASSERT_TRUE(controller->user_capture_region().IsEmpty()); // Hit space when nothing is focused to get the expected default capture // region. SendKey(ui::VKEY_SPACE, event_generator); const gfx::Rect expected_default_region = controller->user_capture_region(); SelectRegion(gfx::Rect(20, 20, 200, 200)); // Hit space when there is an existing region. Tests that the region remains // unchanged. SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(gfx::Rect(20, 20, 200, 200), controller->user_capture_region()); // Tab to the image toggle button. Tests that hitting space does not change // the region size. SelectRegion(gfx::Rect()); SendKey(ui::VKEY_TAB, event_generator); CaptureModeSessionTestApi test_api(controller->capture_mode_session()); ASSERT_EQ(CaptureModeSessionFocusCycler::FocusGroup::kTypeSource, test_api.GetCurrentFocusGroup()); ASSERT_EQ(0u, test_api.GetCurrentFocusIndex()); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(gfx::Rect(), controller->user_capture_region()); // Tests that hitting space while focusing the region toggle button when in // region capture mode will make the capture region the default size. // SelectRegion removes focus since it uses mouse clicks. SelectRegion(gfx::Rect()); SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/4); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(expected_default_region, controller->user_capture_region()); // Tests that hitting space while focusing the region toggle button when not // in region capture mode does nothing to the capture region. SelectRegion(gfx::Rect()); ClickOnView(GetWindowToggleButton(), event_generator); SendKey(ui::VKEY_TAB, event_generator, ui::EF_NONE, /*count=*/4); ASSERT_EQ(CaptureModeSessionFocusCycler::FocusGroup::kTypeSource, test_api.GetCurrentFocusGroup()); ASSERT_EQ(3u, test_api.GetCurrentFocusIndex()); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(gfx::Rect(), controller->user_capture_region()); } // Tests that accessibility overrides are set as expected on capture mode // widgets. TEST_F(CaptureModeTest, AccessibilityFocusAnnotator) { StartImageRegionCapture(); // Helper that takes in a current widget and checks if the accessibility next // and previous focus widgets match the given. auto check_a11y_overrides = [](const std::string& id, views::Widget* widget, views::Widget* expected_previous, views::Widget* expected_next) -> void { SCOPED_TRACE(id); views::View* contents_view = widget->GetContentsView(); views::ViewAccessibility& view_accessibility = contents_view->GetViewAccessibility(); EXPECT_EQ(expected_previous, view_accessibility.GetPreviousFocus()); EXPECT_EQ(expected_next, view_accessibility.GetNextFocus()); }; // With no region, there is no capture label button and no settings menu // opened, so the bar is the only focusable capture session widget. views::Widget* bar_widget = GetCaptureModeBarWidget(); check_a11y_overrides("bar", bar_widget, nullptr, nullptr); // With a region, the focus should go from the bar widget to the label widget // and back. SendKey(ui::VKEY_SPACE, GetEventGenerator()); views::Widget* label_widget = GetCaptureModeLabelWidget(); check_a11y_overrides("bar", bar_widget, label_widget, label_widget); check_a11y_overrides("label", label_widget, bar_widget, bar_widget); // With a settings menu open, the focus should go from the bar widget to the // label widget to the settings widget and back to the bar widget. ClickOnView(GetSettingsButton(), GetEventGenerator()); views::Widget* settings_widget = GetCaptureModeSettingsWidget(); ASSERT_TRUE(settings_widget); check_a11y_overrides("bar", bar_widget, settings_widget, label_widget); check_a11y_overrides("label", label_widget, bar_widget, settings_widget); check_a11y_overrides("settings", settings_widget, label_widget, bar_widget); } // Tests that a captured image is written to the clipboard. TEST_F(CaptureModeTest, ClipboardWrite) { auto* clipboard = ui::Clipboard::GetForCurrentThread(); ASSERT_NE(clipboard, nullptr); const ui::ClipboardSequenceNumberToken before_sequence_number = clipboard->GetSequenceNumber(ui::ClipboardBuffer::kCopyPaste); CaptureNotificationWaiter waiter; CaptureModeController::Get()->CaptureScreenshotsOfAllDisplays(); waiter.Wait(); const ui::ClipboardSequenceNumberToken after_sequence_number = clipboard->GetSequenceNumber(ui::ClipboardBuffer::kCopyPaste); EXPECT_NE(before_sequence_number, after_sequence_number); } // Tests the reverse tabbing behavior of the keyboard navigation. TEST_F(CaptureModeTest, ReverseTabbingTest) { using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; auto* event_generator = GetEventGenerator(); for (CaptureModeSource source : {CaptureModeSource::kFullscreen, CaptureModeSource::kRegion, CaptureModeSource::kWindow}) { auto* controller = StartCaptureSession(source, CaptureModeType::kImage); CaptureModeSessionTestApi test_api(controller->capture_mode_session()); // Nothing is focused initially. EXPECT_EQ(FocusGroup::kNone, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); // Reverse tabbing once and the focus should be on the close button. SendKey(ui::VKEY_TAB, event_generator, ui::EF_SHIFT_DOWN); EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup()); EXPECT_TRUE(GetCloseButton()->has_focus()); controller->Stop(); } } // A regression test for a UAF issue reported at https://crbug.com/1350743, in // which if a the native widget of the settings menu gets deleted without // calling `Close()` or `CloseNow()` on the widget, we get a UAF. This can // happen when all the windows in the window tree hierarchy gets deleted e.g. // when shutting down. TEST_F(CaptureModeTest, SettingsMenuWidgetDestruction) { CaptureModeTestApi().StartForFullscreen(true); ClickOnView(GetSettingsButton(), GetEventGenerator()); auto* widget = GetCaptureModeSettingsWidget(); ASSERT_TRUE(widget); delete widget->GetNativeWindow(); } // A test class that uses a mock time task environment. class CaptureModeMockTimeTest : public CaptureModeTest { public: CaptureModeMockTimeTest() : CaptureModeTest(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} CaptureModeMockTimeTest(const CaptureModeMockTimeTest&) = delete; CaptureModeMockTimeTest& operator=(const CaptureModeMockTimeTest&) = delete; ~CaptureModeMockTimeTest() override = default; }; // Tests that the consecutive screenshots histogram is recorded properly. TEST_F(CaptureModeMockTimeTest, ConsecutiveScreenshotsHistograms) { constexpr char kConsecutiveScreenshotsHistogram[] = "Ash.CaptureModeController.ConsecutiveScreenshots"; base::HistogramTester histogram_tester; auto take_n_screenshots = [this](int n) { for (int i = 0; i < n; ++i) { auto* controller = StartImageRegionCapture(); controller->PerformCapture(); } }; // Take three consecutive screenshots. Should only record after 5 seconds. StartImageRegionCapture(); const gfx::Rect capture_region(200, 200, 400, 400); SelectRegion(capture_region); take_n_screenshots(3); histogram_tester.ExpectBucketCount(kConsecutiveScreenshotsHistogram, 3, 0); task_environment()->FastForwardBy(base::Seconds(5)); histogram_tester.ExpectBucketCount(kConsecutiveScreenshotsHistogram, 3, 1); // Take only one screenshot. This should not be recorded. take_n_screenshots(1); histogram_tester.ExpectBucketCount(kConsecutiveScreenshotsHistogram, 1, 0); task_environment()->FastForwardBy(base::Seconds(5)); histogram_tester.ExpectBucketCount(kConsecutiveScreenshotsHistogram, 1, 0); // Take a screenshot, change source and take another screenshot. This should // count as 2 consecutive screenshots. take_n_screenshots(1); auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); controller->PerformCapture(); histogram_tester.ExpectBucketCount(kConsecutiveScreenshotsHistogram, 2, 0); task_environment()->FastForwardBy(base::Seconds(5)); histogram_tester.ExpectBucketCount(kConsecutiveScreenshotsHistogram, 2, 1); } // Tests that the user capture region will be cleared up after a period of time. TEST_F(CaptureModeMockTimeTest, ClearUserCaptureRegionBetweenSessions) { UpdateDisplay("900x800"); auto* controller = StartImageRegionCapture(); EXPECT_EQ(gfx::Rect(), controller->user_capture_region()); const gfx::Rect capture_region(100, 100, 600, 700); SelectRegion(capture_region); EXPECT_EQ(capture_region, controller->user_capture_region()); controller->PerformCapture(); EXPECT_EQ(capture_region, controller->user_capture_region()); // Start region image capture again shortly after the previous capture // session, we should still be able to reuse the previous capture region. task_environment()->FastForwardBy(base::Minutes(1)); StartImageRegionCapture(); EXPECT_EQ(capture_region, controller->user_capture_region()); auto* event_generator = GetEventGenerator(); // Even if the capture is cancelled, we still remember the capture region. SendKey(ui::VKEY_ESCAPE, event_generator); EXPECT_EQ(capture_region, controller->user_capture_region()); // Wait for 8 second and then start region image capture again. We should have // forgot the previous capture region. task_environment()->FastForwardBy(base::Minutes(8)); StartImageRegionCapture(); EXPECT_EQ(gfx::Rect(), controller->user_capture_region()); } // Tests that in Region mode, the capture bar hides and shows itself correctly. TEST_F(CaptureModeTest, CaptureBarOpacity) { UpdateDisplay("800x700"); auto* event_generator = GetEventGenerator(); auto* controller = StartImageRegionCapture(); EXPECT_TRUE(controller->IsActive()); ui::Layer* capture_bar_layer = GetCaptureModeBarWidget()->GetLayer(); // Check to see it starts off opaque. EXPECT_EQ(1.f, capture_bar_layer->GetTargetOpacity()); // Make sure that the bar is transparent when selecting a region. const gfx::Rect target_region(gfx::BoundingRect( gfx::Point(0, 0), GetCaptureModeBarView()->GetBoundsInScreen().top_right() + gfx::Vector2d(0, -50))); event_generator->MoveMouseTo(target_region.origin()); event_generator->PressLeftButton(); EXPECT_EQ(0.f, capture_bar_layer->GetTargetOpacity()); event_generator->MoveMouseTo(target_region.bottom_right()); EXPECT_EQ(0.f, capture_bar_layer->GetTargetOpacity()); event_generator->ReleaseLeftButton(); // When there is no overlap of the selected region and the bar, the bar should // be opaque. EXPECT_EQ(1.f, capture_bar_layer->GetTargetOpacity()); // Bar becomes transparent when the region is being moved. event_generator->MoveMouseTo(target_region.origin() + gfx::Vector2d(50, 50)); event_generator->PressLeftButton(); EXPECT_EQ(0.f, capture_bar_layer->GetTargetOpacity()); event_generator->MoveMouseTo(target_region.bottom_center()); EXPECT_EQ(0.f, capture_bar_layer->GetTargetOpacity()); event_generator->ReleaseLeftButton(); // The region overlaps the capture bar, so we set the opacity of the bar to // the overlapped opacity. EXPECT_EQ(capture_mode::kCaptureUiOverlapOpacity, capture_bar_layer->GetTargetOpacity()); // When there is overlap, the toolbar turns opaque on mouseover. event_generator->MoveMouseTo( GetCaptureModeBarView()->GetBoundsInScreen().CenterPoint()); EXPECT_EQ(1.f, capture_bar_layer->GetTargetOpacity()); // Capture bar drops back to the overlapped opacity when the mouse is no // longer hovering. event_generator->MoveMouseTo( GetCaptureModeBarView()->GetBoundsInScreen().top_center() + gfx::Vector2d(0, -50)); EXPECT_EQ(capture_mode::kCaptureUiOverlapOpacity, capture_bar_layer->GetTargetOpacity()); // Check that the opacity is reset when we select another region. SelectRegion(target_region); EXPECT_EQ(1.f, capture_bar_layer->GetTargetOpacity()); } TEST_F(CaptureModeTest, CaptureBarOpacityOnHoveringOnCaptureLabel) { UpdateDisplay("800x700"); auto* event_generator = GetEventGenerator(); auto* controller = StartImageRegionCapture(); EXPECT_TRUE(controller->IsActive()); ui::Layer* capture_bar_layer = GetCaptureModeBarWidget()->GetLayer(); // Set the capture region to make it overlap with the capture bar. And then // move the mouse to the outside of the capture bar, verify it has the // overlapped opacity. const gfx::Rect capture_region(200, 500, 130, 130); SelectRegion(capture_region); event_generator->MoveMouseTo({10, 10}); EXPECT_EQ(capture_mode::kCaptureUiOverlapOpacity, capture_bar_layer->GetTargetOpacity()); // Move mouse on top of the capture label, verify the bar becomes fully // opaque. event_generator->MoveMouseTo( GetCaptureModeLabelWidget()->GetWindowBoundsInScreen().CenterPoint()); EXPECT_EQ(1.f, capture_bar_layer->GetTargetOpacity()); } // Tests that the quick action histogram is recorded properly. TEST_F(CaptureModeTest, QuickActionHistograms) { constexpr char kQuickActionHistogramName[] = "Ash.CaptureModeController.QuickAction"; base::HistogramTester histogram_tester; auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); EXPECT_TRUE(controller->IsActive()); { CaptureNotificationWaiter waiter; controller->PerformCapture(); waiter.Wait(); } // Verify clicking delete on screenshot notification. base::RunLoop loop; SetUpFileDeletionVerifier(&loop); const int delete_button = 1; ClickNotification(delete_button); loop.Run(); EXPECT_FALSE(GetPreviewNotification()); histogram_tester.ExpectBucketCount(kQuickActionHistogramName, CaptureQuickAction::kDelete, 1); controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); { CaptureNotificationWaiter waiter; controller->PerformCapture(); waiter.Wait(); } // Click on the notification body. This should take us to the files app. ClickNotification(absl::nullopt); EXPECT_FALSE(GetPreviewNotification()); histogram_tester.ExpectBucketCount(kQuickActionHistogramName, CaptureQuickAction::kFiles, 1); controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); { CaptureNotificationWaiter waiter; controller->PerformCapture(); waiter.Wait(); } const int edit_button = 0; // Verify clicking edit on screenshot notification. ClickNotification(edit_button); EXPECT_FALSE(GetPreviewNotification()); histogram_tester.ExpectBucketCount(kQuickActionHistogramName, CaptureQuickAction::kBacklight, 1); } TEST_F(CaptureModeTest, CannotDoMultipleRecordings) { StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); auto* controller = CaptureModeController::Get(); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); EXPECT_EQ(CaptureModeType::kVideo, controller->type()); // Start a new session with the current type which set to kVideo, the type // should be switched automatically to kImage, and video toggle button should // be disabled. controller->Start(CaptureModeEntryType::kQuickSettings); EXPECT_TRUE(controller->IsActive()); EXPECT_EQ(CaptureModeType::kImage, controller->type()); EXPECT_TRUE(GetImageToggleButton()->GetToggled()); EXPECT_FALSE(GetVideoToggleButton()->GetToggled()); EXPECT_FALSE(GetVideoToggleButton()->GetEnabled()); // Clicking on the video button should do nothing. ClickOnView(GetVideoToggleButton(), GetEventGenerator()); EXPECT_TRUE(GetImageToggleButton()->GetToggled()); EXPECT_FALSE(GetVideoToggleButton()->GetToggled()); EXPECT_EQ(CaptureModeType::kImage, controller->type()); // Things should go back to normal when there's no recording going on. controller->Stop(); controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton); StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); EXPECT_EQ(CaptureModeType::kVideo, controller->type()); EXPECT_FALSE(GetImageToggleButton()->GetToggled()); EXPECT_TRUE(GetVideoToggleButton()->GetToggled()); EXPECT_TRUE(GetVideoToggleButton()->GetEnabled()); } // Tests the basic settings menu functionality. TEST_F(CaptureModeTest, SettingsMenuVisibilityBasic) { auto* event_generator = GetEventGenerator(); auto* controller = StartImageRegionCapture(); EXPECT_TRUE(controller->IsActive()); // Session starts with settings menu not initialized. EXPECT_FALSE(GetCaptureModeSettingsWidget()); // Test clicking the settings button toggles the button as well as // opens/closes the settings menu. ClickOnView(GetSettingsButton(), event_generator); EXPECT_TRUE(GetCaptureModeSettingsWidget()); EXPECT_TRUE(GetSettingsButton()->GetToggled()); ClickOnView(GetSettingsButton(), event_generator); EXPECT_FALSE(GetCaptureModeSettingsWidget()); EXPECT_FALSE(GetSettingsButton()->GetToggled()); } // Tests how interacting with the rest of the screen (i.e. clicking outside of // the bar/menu, on other buttons) affects whether the settings menu should // close or not. TEST_F(CaptureModeTest, SettingsMenuVisibilityClicking) { UpdateDisplay("800x700"); auto* event_generator = GetEventGenerator(); auto* controller = StartImageRegionCapture(); EXPECT_TRUE(controller->IsActive()); // Test clicking on the option of settings menu doesn't close the // settings menu. ClickOnView(GetSettingsButton(), event_generator); ClickOnView(GetCaptureModeSettingsView(), event_generator); EXPECT_TRUE(GetCaptureModeSettingsWidget()); EXPECT_TRUE(GetSettingsButton()->GetToggled()); CaptureModeSettingsTestApi test_api; ClickOnView(test_api.GetAudioOffOption(), event_generator); EXPECT_TRUE(GetCaptureModeSettingsWidget()); EXPECT_TRUE(GetSettingsButton()->GetToggled()); // Test clicking on the capture bar closes the settings menu. event_generator->MoveMouseTo( GetCaptureModeBarView()->GetBoundsInScreen().top_center() + gfx::Vector2d(0, 2)); event_generator->ClickLeftButton(); EXPECT_FALSE(GetCaptureModeSettingsWidget()); EXPECT_FALSE(GetSettingsButton()->GetToggled()); // Test clicking on a different source closes the settings menu. ClickOnView(GetSettingsButton(), event_generator); ClickOnView(GetFullscreenToggleButton(), event_generator); EXPECT_FALSE(GetCaptureModeSettingsWidget()); // Test clicking on a different type closes the settings menu. ClickOnView(GetSettingsButton(), event_generator); ClickOnView(GetVideoToggleButton(), event_generator); EXPECT_FALSE(GetCaptureModeSettingsWidget()); // Exit the capture session with the settings menu open, and test to make sure // the new session starts with the settings menu hidden. ClickOnView(GetSettingsButton(), event_generator); SendKey(ui::VKEY_ESCAPE, event_generator); StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); EXPECT_FALSE(GetCaptureModeSettingsWidget()); // Take a screenshot with the settings menu open, and test to make sure the // new session starts with the settings menu hidden. ClickOnView(GetSettingsButton(), event_generator); // Take screenshot. SendKey(ui::VKEY_RETURN, event_generator); StartImageRegionCapture(); EXPECT_FALSE(GetCaptureModeSettingsWidget()); } // Tests capture bar and settings menu visibility / opacity when capture region // is being or after drawn. TEST_F(CaptureModeTest, CaptureBarAndSettingsMenuVisibilityDrawingRegion) { UpdateDisplay("800x700"); auto* event_generator = GetEventGenerator(); auto* controller = StartImageRegionCapture(); auto* capture_bar_widget = GetCaptureModeBarWidget(); ui::Layer* capture_bar_layer = capture_bar_widget->GetLayer(); EXPECT_TRUE(controller->IsActive()); auto* session = CaptureModeController::Get()->capture_mode_session(); // Test the settings menu and capture bar are hidden when the user clicks to // start selecting a region. ClickOnView(GetSettingsButton(), event_generator); EXPECT_TRUE(GetCaptureModeSettingsWidget()); const gfx::Rect target_region(gfx::BoundingRect( gfx::Point(0, 0), capture_bar_widget->GetWindowBoundsInScreen().top_right() + gfx::Vector2d(0, -50))); // Moving the cursor outside the bounds of the settings menu should update the // cursor to `kPointer`, since the only possible operation here when clicking // is to dismiss the settings menu rather than take a screenshot or update the // region. event_generator->MoveMouseTo(target_region.origin()); auto* cursor_manager = Shell::Get()->cursor_manager(); using ui::mojom::CursorType; EXPECT_EQ(CursorType::kPointer, cursor_manager->GetCursor().type()); // Pressing outside the bounds of the settings should dismiss it immediately, // update the cursor to `kCell` (to signal that it's now possible to select a // region), but region selection doesn't start until the next click event. event_generator->PressLeftButton(); EXPECT_FALSE(GetCaptureModeSettingsWidget()); EXPECT_EQ(CursorType::kCell, cursor_manager->GetCursor().type()); EXPECT_FALSE(session->is_selecting_region()); event_generator->ReleaseLeftButton(); EXPECT_FALSE(session->is_selecting_region()); event_generator->PressLeftButton(); EXPECT_TRUE(session->is_selecting_region()); event_generator->MoveMouseTo(target_region.bottom_right()); EXPECT_EQ(0.f, capture_bar_layer->GetTargetOpacity()); event_generator->ReleaseLeftButton(); EXPECT_FALSE(GetCaptureModeSettingsWidget()); // Test that the settings menu will dismiss immediately when clicking // somewhere in the middle of the capture region. ClickOnView(GetSettingsButton(), event_generator); event_generator->MoveMouseTo(target_region.origin() + gfx::Vector2d(50, 50)); event_generator->PressLeftButton(); EXPECT_FALSE(GetCaptureModeSettingsWidget()); event_generator->ReleaseLeftButton(); event_generator->PressLeftButton(); EXPECT_EQ(CursorType::kMove, cursor_manager->GetCursor().type()); EXPECT_FALSE(session->is_selecting_region()); EXPECT_TRUE(session->is_drag_in_progress()); // This creates a region that overlaps with the capture bar. The capture bar // should be fully transparent while dragging the region is in progress. event_generator->MoveMouseTo(target_region.bottom_center()); EXPECT_EQ(0.f, capture_bar_layer->GetTargetOpacity()); event_generator->ReleaseLeftButton(); // With an overlapping region (as dragged to above), the capture bar opacity // is changed based on hover. If the settings menu is open/visible, the // capture bar will always be visible no matter if the mouse is hovered on it // or not. event_generator->MoveMouseTo(target_region.origin()); EXPECT_EQ(capture_mode::kCaptureUiOverlapOpacity, capture_bar_layer->GetTargetOpacity()); // Move mouse on top of the capture bar, verify that capture bar becomes // visible. event_generator->MoveMouseTo( capture_bar_widget->GetWindowBoundsInScreen().CenterPoint()); EXPECT_EQ(1.f, capture_bar_layer->GetTargetOpacity()); ClickOnView(GetSettingsButton(), event_generator); EXPECT_TRUE(GetCaptureModeSettingsWidget()); EXPECT_EQ(1.f, capture_bar_layer->GetTargetOpacity()); // Move mouse onto the settings menu, confirm the capture bar is still // visible. event_generator->MoveMouseTo( GetCaptureModeSettingsView()->GetBoundsInScreen().CenterPoint()); EXPECT_EQ(1.f, capture_bar_layer->GetTargetOpacity()); // Move mouse to the outside of the capture bar and settings, verify that // settings menu are still open and both capture bar and settings have full // opaque. event_generator->MoveMouseTo(target_region.origin()); auto* settings_menu = GetCaptureModeSettingsView(); EXPECT_TRUE(settings_menu); EXPECT_EQ(1.f, capture_bar_layer->GetTargetOpacity()); EXPECT_EQ(1.f, settings_menu->layer()->GetTargetOpacity()); // Close settings menu, and move mouse to the outside of the capture bar, // verify capture bar has the overlapped opacity. ClickOnView(GetSettingsButton(), event_generator); event_generator->MoveMouseTo(target_region.origin()); EXPECT_EQ(capture_mode::kCaptureUiOverlapOpacity, capture_bar_layer->GetTargetOpacity()); } TEST_F(CaptureModeTest, CaptureFolderSetting) { auto* controller = CaptureModeController::Get(); auto* test_delegate = controller->delegate_for_testing(); const auto default_downloads_folder = test_delegate->GetUserDefaultDownloadsFolder(); auto capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_EQ(capture_folder.path, default_downloads_folder); EXPECT_TRUE(capture_folder.is_default_downloads_folder); const base::FilePath custom_folder(FILE_PATH_LITERAL("/home/tests")); controller->SetCustomCaptureFolder(custom_folder); capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_EQ(capture_folder.path, custom_folder); EXPECT_FALSE(capture_folder.is_default_downloads_folder); } TEST_F(CaptureModeTest, CaptureFolderSetToDefaultDownloads) { auto* controller = CaptureModeController::Get(); auto* test_delegate = controller->delegate_for_testing(); const base::FilePath custom_folder(FILE_PATH_LITERAL("/home/tests")); controller->SetCustomCaptureFolder(custom_folder); auto capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_FALSE(capture_folder.is_default_downloads_folder); // If the user selects the default downloads folder manually, we should be // able to detect that. const auto default_downloads_folder = test_delegate->GetUserDefaultDownloadsFolder(); controller->SetCustomCaptureFolder(default_downloads_folder); capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_EQ(capture_folder.path, default_downloads_folder); EXPECT_TRUE(capture_folder.is_default_downloads_folder); } TEST_F(CaptureModeTest, UsesDefaultFolderWithCustomFolderSet) { auto* controller = CaptureModeController::Get(); auto* test_delegate = controller->delegate_for_testing(); const base::FilePath custom_folder(FILE_PATH_LITERAL("/home/tests")); controller->SetCustomCaptureFolder(custom_folder); auto capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_FALSE(capture_folder.is_default_downloads_folder); // If the user selects to force use the default downloads folder even while // a custom folder is set, we should respect that, but we shouldn't clear the // custom folder. controller->SetUsesDefaultCaptureFolder(true); const auto default_downloads_folder = test_delegate->GetUserDefaultDownloadsFolder(); capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_EQ(capture_folder.path, default_downloads_folder); EXPECT_TRUE(capture_folder.is_default_downloads_folder); // Setting another custom folder value, would reset the // "UsesDefaultCaptureFolder" value, and the new custom folder will be used. const base::FilePath custom_folder2(FILE_PATH_LITERAL("/home/tests2")); controller->SetCustomCaptureFolder(custom_folder2); capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_EQ(capture_folder.path, custom_folder2); EXPECT_FALSE(capture_folder.is_default_downloads_folder); } TEST_F(CaptureModeTest, CaptureFolderSetToEmptyPath) { auto* controller = CaptureModeController::Get(); auto* test_delegate = controller->delegate_for_testing(); const base::FilePath custom_folder(FILE_PATH_LITERAL("/home/tests")); controller->SetCustomCaptureFolder(custom_folder); auto capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_FALSE(capture_folder.is_default_downloads_folder); // If we set the custom path to an empty folder to clear, we should revert // back to the default downloads folder. controller->SetCustomCaptureFolder(base::FilePath()); const auto default_downloads_folder = test_delegate->GetUserDefaultDownloadsFolder(); capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_EQ(capture_folder.path, default_downloads_folder); EXPECT_TRUE(capture_folder.is_default_downloads_folder); } TEST_F(CaptureModeTest, SimulateUserCancelingDlpWarningDialog) { auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); // Simulate the user canceling the DLP warning dialog at the end of the // recording which is shown to the user to alert about restricted content // showing up on the screen during the recording. In this case, the user // requests the deletion of the file. auto* test_delegate = static_cast(controller->delegate_for_testing()); test_delegate->set_should_save_after_dlp_check(false); base::RunLoop loop; SetUpFileDeletionVerifier(&loop); controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton); loop.Run(); // No notification should show in this case, nor any thing on Tote. EXPECT_FALSE(GetPreviewNotification()); ash::HoldingSpaceTestApi holding_space_api; EXPECT_TRUE(holding_space_api.GetScreenCaptureViews().empty()); } namespace { // ----------------------------------------------------------------------------- // TestVideoCaptureOverlay: // Defines a fake video capture overlay to be used in testing the behavior of // the cursor overlay. The VideoRecordingWatcher will control this overlay via // mojo. using Overlay = viz::mojom::FrameSinkVideoCaptureOverlay; class TestVideoCaptureOverlay : public Overlay { public: explicit TestVideoCaptureOverlay(mojo::PendingReceiver receiver) : receiver_(this, std::move(receiver)) {} ~TestVideoCaptureOverlay() override = default; const gfx::RectF& last_bounds() const { return last_bounds_; } bool IsHidden() const { return last_bounds_ == gfx::RectF(); } // viz::mojom::FrameSinkVideoCaptureOverlay: void SetImageAndBounds(const SkBitmap& image, const gfx::RectF& bounds) override { last_bounds_ = bounds; } void SetBounds(const gfx::RectF& bounds) override { last_bounds_ = bounds; } private: mojo::Receiver receiver_; gfx::RectF last_bounds_; }; // ----------------------------------------------------------------------------- // CaptureModeCursorOverlayTest: // Defines a test fixure to test the behavior of the cursor overlay. class CaptureModeCursorOverlayTest : public CaptureModeTest { public: CaptureModeCursorOverlayTest() = default; ~CaptureModeCursorOverlayTest() override = default; aura::Window* window() const { return window_.get(); } TestVideoCaptureOverlay* fake_overlay() const { return fake_overlay_.get(); } // CaptureModeTest: void SetUp() override { CaptureModeTest::SetUp(); window_ = CreateTestWindow(gfx::Rect(200, 200)); } void TearDown() override { window_.reset(); CaptureModeTest::TearDown(); } CaptureModeController* StartRecordingAndSetupFakeOverlay( CaptureModeSource source) { auto* controller = StartCaptureSession(source, CaptureModeType::kVideo); auto* event_generator = GetEventGenerator(); if (source == CaptureModeSource::kWindow) event_generator->MoveMouseToCenterOf(window_.get()); StartVideoRecordingImmediately(); EXPECT_TRUE(controller->is_recording_in_progress()); auto* recording_watcher = controller->video_recording_watcher_for_testing(); mojo::PendingRemote overlay_pending_remote; fake_overlay_ = std::make_unique( overlay_pending_remote.InitWithNewPipeAndPassReceiver()); recording_watcher->BindCursorOverlayForTesting( std::move(overlay_pending_remote)); // The overlay should be initially hidden until a mourse event is received. FlushOverlay(); EXPECT_TRUE(fake_overlay()->IsHidden()); // Generating some mouse events may or may not show the overlay, depending // on the conditions of the test. Each test will verify its expectation // after this returns. event_generator->MoveMouseBy(10, 10); FlushOverlay(); return controller; } void FlushOverlay() { auto* controller = CaptureModeController::Get(); DCHECK(controller->is_recording_in_progress()); controller->video_recording_watcher_for_testing() ->FlushCursorOverlayForTesting(); } // The docked magnifier is one of the features that force the software- // composited cursor to be used when enabled. We use it to test the behavior // of the cursor overlay in that case. void SetDockedMagnifierEnabled(bool enabled) { Shell::Get()->docked_magnifier_controller()->SetEnabled(enabled); } // Checks that capturing a screenshot hides the cursor. After the capture is // complete, checks that the cursor returns to the previous state, i.e. // hidden for tablet mode but visible for clamshell mode. void CaptureScreenshotAndCheckCursorVisibility( CaptureModeController* controller) { EXPECT_EQ(controller->type(), CaptureModeType::kImage); auto* shell = Shell::Get(); auto* cursor_manager = shell->cursor_manager(); bool in_tablet_mode = shell->tablet_mode_controller()->InTabletMode(); // The capture mode session locks the cursor for the whole active session // except in the tablet mode unless the cursor is visible. EXPECT_EQ(!in_tablet_mode, cursor_manager->IsCursorLocked()); EXPECT_EQ(!in_tablet_mode, cursor_manager->IsCursorVisible()); EXPECT_TRUE(controller->IsActive()); // Make sure the cursor is hidden while capturing the screenshot. CaptureNotificationWaiter waiter; controller->PerformCapture(); EXPECT_FALSE(cursor_manager->IsCursorVisible()); EXPECT_FALSE(controller->IsActive()); // The cursor visibility should be restored after the capture is done. waiter.Wait(); EXPECT_EQ(!in_tablet_mode, cursor_manager->IsCursorVisible()); EXPECT_FALSE(cursor_manager->IsCursorLocked()); } private: std::unique_ptr window_; std::unique_ptr fake_overlay_; }; } // namespace TEST_F(CaptureModeCursorOverlayTest, TabletModeHidesCursorOverlay) { StartRecordingAndSetupFakeOverlay(CaptureModeSource::kFullscreen); EXPECT_FALSE(fake_overlay()->IsHidden()); // Entering tablet mode should hide the cursor overlay. SwitchToTabletMode(); FlushOverlay(); EXPECT_TRUE(fake_overlay()->IsHidden()); // Exiting tablet mode should reshow the overlay. LeaveTabletMode(); FlushOverlay(); EXPECT_FALSE(fake_overlay()->IsHidden()); } // Tests that the cursor is hidden while taking a screenshot in tablet mode and // remains hidden afterward. TEST_F(CaptureModeCursorOverlayTest, TabletModeHidesCursor) { // Enter tablet mode. SwitchToTabletMode(); auto* cursor_manager = Shell::Get()->cursor_manager(); CaptureModeController* controller = StartCaptureSession( CaptureModeSource::kFullscreen, CaptureModeType::kImage); // Test the hardware cursor. CaptureScreenshotAndCheckCursorVisibility(controller); // Test the software cursor enabled by docked magnifier. SetDockedMagnifierEnabled(true); EXPECT_TRUE(IsCursorCompositingEnabled()); controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); CaptureScreenshotAndCheckCursorVisibility(controller); // Exiting tablet mode. LeaveTabletMode(); EXPECT_TRUE(cursor_manager->IsCursorVisible()); } // Tests that a cursor is hidden while taking a fullscreen screenshot // (crbug.com/1186652). TEST_F(CaptureModeCursorOverlayTest, CursorInFullscreenScreenshot) { auto* cursor_manager = Shell::Get()->cursor_manager(); EXPECT_FALSE(cursor_manager->IsCursorLocked()); CaptureModeController* controller = StartCaptureSession( CaptureModeSource::kFullscreen, CaptureModeType::kImage); auto* event_generator = GetEventGenerator(); event_generator->MoveMouseTo(gfx::Point(175, 175)); // Test the hardware cursor. CaptureScreenshotAndCheckCursorVisibility(controller); // Test the software cursor enabled by docked magnifier. SetDockedMagnifierEnabled(true); EXPECT_TRUE(IsCursorCompositingEnabled()); controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); CaptureScreenshotAndCheckCursorVisibility(controller); } // Tests that a cursor is hidden while taking a region screenshot // (crbug.com/1186652). TEST_F(CaptureModeCursorOverlayTest, CursorInPartialRegionScreenshot) { // Use a set display size as we will be choosing points in this test. UpdateDisplay("800x700"); auto* cursor_manager = Shell::Get()->cursor_manager(); EXPECT_FALSE(cursor_manager->IsCursorLocked()); auto* event_generator = GetEventGenerator(); auto* controller = StartImageRegionCapture(); // Create the initial capture region. const gfx::Rect target_region(gfx::Rect(50, 50, 200, 200)); SelectRegion(target_region); event_generator->MoveMouseTo(gfx::Point(175, 175)); // Test the hardware cursor. CaptureScreenshotAndCheckCursorVisibility(controller); // Test the software cursor enabled by docked magnifier. SetDockedMagnifierEnabled(true); EXPECT_TRUE(IsCursorCompositingEnabled()); controller = StartImageRegionCapture(); CaptureScreenshotAndCheckCursorVisibility(controller); } TEST_F(CaptureModeCursorOverlayTest, SoftwareCursorInitiallyEnabled) { // The software cursor is enabled before recording starts. SetDockedMagnifierEnabled(true); EXPECT_TRUE(IsCursorCompositingEnabled()); // Hence the overlay will be hidden initially. StartRecordingAndSetupFakeOverlay(CaptureModeSource::kFullscreen); EXPECT_TRUE(fake_overlay()->IsHidden()); } TEST_F(CaptureModeCursorOverlayTest, SoftwareCursorInFullscreenRecording) { StartRecordingAndSetupFakeOverlay(CaptureModeSource::kFullscreen); EXPECT_FALSE(fake_overlay()->IsHidden()); // When the software-composited cursor is enabled, the overlay is hidden to // avoid having two overlapping cursors in the video. SetDockedMagnifierEnabled(true); EXPECT_TRUE(IsCursorCompositingEnabled()); FlushOverlay(); EXPECT_TRUE(fake_overlay()->IsHidden()); SetDockedMagnifierEnabled(false); EXPECT_FALSE(IsCursorCompositingEnabled()); FlushOverlay(); EXPECT_FALSE(fake_overlay()->IsHidden()); } TEST_F(CaptureModeCursorOverlayTest, SoftwareCursorInPartialRegionRecording) { CaptureModeController::Get()->SetUserCaptureRegion(gfx::Rect(20, 20), /*by_user=*/true); StartRecordingAndSetupFakeOverlay(CaptureModeSource::kRegion); EXPECT_FALSE(fake_overlay()->IsHidden()); // The behavior in this case is exactly the same as in fullscreen recording. SetDockedMagnifierEnabled(true); EXPECT_TRUE(IsCursorCompositingEnabled()); FlushOverlay(); EXPECT_TRUE(fake_overlay()->IsHidden()); } TEST_F(CaptureModeCursorOverlayTest, SoftwareCursorInWindowRecording) { StartRecordingAndSetupFakeOverlay(CaptureModeSource::kWindow); EXPECT_FALSE(fake_overlay()->IsHidden()); // When recording a window, the software cursor has no effect of the cursor // overlay, since the cursor widget is not in the recorded window subtree, so // it cannot be captured by the frame sink capturer. We have to provide cursor // capturing through the overlay. SetDockedMagnifierEnabled(true); EXPECT_TRUE(IsCursorCompositingEnabled()); FlushOverlay(); EXPECT_FALSE(fake_overlay()->IsHidden()); } TEST_F(CaptureModeCursorOverlayTest, OverlayHidesWhenOutOfBounds) { StartRecordingAndSetupFakeOverlay(CaptureModeSource::kWindow); EXPECT_FALSE(fake_overlay()->IsHidden()); const gfx::Point bottom_right = window()->GetBoundsInRootWindow().bottom_right(); auto* generator = GetEventGenerator(); // Generate a click event to overcome throttling. generator->MoveMouseTo(bottom_right); generator->ClickLeftButton(); FlushOverlay(); EXPECT_TRUE(fake_overlay()->IsHidden()); } // Verifies that the cursor overlay bounds calculation takes into account the // cursor image scale factor. https://crbug.com/1222494. TEST_F(CaptureModeCursorOverlayTest, OverlayBoundsAccountForCursorScaleFactor) { UpdateDisplay("500x400"); StartRecordingAndSetupFakeOverlay(CaptureModeSource::kFullscreen); EXPECT_FALSE(fake_overlay()->IsHidden()); auto* cursor_manager = Shell::Get()->cursor_manager(); auto set_cursor = [cursor_manager](const gfx::Size& cursor_image_size, float cursor_image_scale_factor) { const auto cursor_type = ui::mojom::CursorType::kCustom; gfx::NativeCursor cursor{cursor_type}; SkBitmap cursor_image; cursor_image.allocN32Pixels(cursor_image_size.width(), cursor_image_size.height()); cursor.set_image_scale_factor(cursor_image_scale_factor); cursor.set_custom_bitmap(cursor_image); auto* platform_cursor_factory = ui::OzonePlatform::GetInstance()->GetCursorFactory(); cursor.SetPlatformCursor(platform_cursor_factory->CreateImageCursor( cursor_type, cursor_image, cursor.custom_hotspot())); cursor_manager->SetCursor(cursor); }; struct { gfx::Size cursor_size; float cursor_image_scale_factor; } kTestCases[] = { { gfx::Size(50, 50), /*cursor_image_scale_factor=*/2.f, }, { gfx::Size(25, 25), /*cursor_image_scale_factor=*/1.f, }, }; // Both of the above test cases should yield the same cursor overlay relative // bounds when the cursor is at the center of the screen. // Origin is 0.5f (center) // Size is 25 (cursor image dip size) / {500,400} = {0.05f, 0.0625f} const gfx::RectF expected_overlay_bounds{0.5f, 0.5f, 0.05f, 0.0625f}; const gfx::Point screen_center = window()->GetRootWindow()->bounds().CenterPoint(); auto* generator = GetEventGenerator(); for (const auto& test_case : kTestCases) { set_cursor(test_case.cursor_size, test_case.cursor_image_scale_factor); // Lock the cursor to prevent mouse events from changing it back to a // default kPointer cursor type. cursor_manager->LockCursor(); // Generate a click event to overcome throttling. generator->MoveMouseTo(screen_center); generator->ClickLeftButton(); FlushOverlay(); EXPECT_FALSE(fake_overlay()->IsHidden()); EXPECT_EQ(expected_overlay_bounds, fake_overlay()->last_bounds()); // Unlock the cursor back. cursor_manager->UnlockCursor(); } } // ----------------------------------------------------------------------------- // TODO(afakhry): Add more cursor overlay tests. // Test fixture to verify capture mode + projector integration. namespace { constexpr char kProjectorCreationFlowHistogramName[] = "Ash.Projector.CreationFlow.ClamshellMode"; } // namespace class ProjectorCaptureModeIntegrationTests : public CaptureModeTest, public ::testing::WithParamInterface { public: ProjectorCaptureModeIntegrationTests() = default; ~ProjectorCaptureModeIntegrationTests() override = default; static constexpr gfx::Rect kUserRegion{20, 50, 60, 70}; MockProjectorClient* projector_client() { return projector_helper_.projector_client(); } aura::Window* window() const { return window_.get(); } // CaptureModeTest: void SetUp() override { CaptureModeTest::SetUp(); projector_helper_.SetUp(); window_ = CreateTestWindow(gfx::Rect(20, 30, 200, 200)); CaptureModeController::Get()->SetUserCaptureRegion(kUserRegion, /*by_user=*/true); } void TearDown() override { window_.reset(); CaptureModeTest::TearDown(); } void StartProjectorModeSession() { projector_helper_.StartProjectorModeSession(); } void StartRecordingForProjectorFromSource(CaptureModeSource source) { StartProjectorModeSession(); auto* controller = CaptureModeController::Get(); controller->SetSource(source); switch (source) { case CaptureModeSource::kFullscreen: case CaptureModeSource::kRegion: break; case CaptureModeSource::kWindow: auto* generator = GetEventGenerator(); generator->MoveMouseTo(window_->GetBoundsInScreen().CenterPoint()); break; } CaptureModeTestApi().PerformCapture(); WaitForRecordingToStart(); EXPECT_TRUE(controller->is_recording_in_progress()); } void VerifyOverlayEnabledState(aura::Window* overlay_window, bool overlay_enabled_state) { if (overlay_enabled_state) { EXPECT_TRUE(overlay_window->IsVisible()); EXPECT_EQ(overlay_window->event_targeting_policy(), aura::EventTargetingPolicy::kTargetAndDescendants); } else { EXPECT_FALSE(overlay_window->IsVisible()); EXPECT_EQ(overlay_window->event_targeting_policy(), aura::EventTargetingPolicy::kNone); } } void VerifyOverlayStacking(aura::Window* overlay_window, aura::Window* window_being_recorded, CaptureModeSource source) { auto* parent = overlay_window->parent(); if (source == CaptureModeSource::kWindow) { // The overlay window should always be the top-most child of the window // being recorded when in window mode. ASSERT_EQ(parent, window_being_recorded); EXPECT_EQ(window_being_recorded->children().back(), overlay_window); } else { auto* menu_container = window_being_recorded->GetRootWindow()->GetChildById( kShellWindowId_MenuContainer); ASSERT_EQ(parent, menu_container); EXPECT_EQ(menu_container->children().front(), overlay_window); } } void VerifyOverlayWindow(aura::Window* overlay_window, CaptureModeSource source) { auto* window_being_recorded = GetWindowBeingRecorded(); VerifyOverlayStacking(overlay_window, window_being_recorded, source); switch (source) { case CaptureModeSource::kFullscreen: case CaptureModeSource::kWindow: EXPECT_EQ(overlay_window->bounds(), gfx::Rect(window_being_recorded->bounds().size())); break; case CaptureModeSource::kRegion: EXPECT_EQ(overlay_window->bounds(), kUserRegion); break; } } aura::Window* GetWindowBeingRecorded() const { auto* controller = CaptureModeController::Get(); DCHECK(controller->is_recording_in_progress()); return controller->video_recording_watcher_for_testing() ->window_being_recorded(); } protected: ProjectorCaptureModeIntegrationHelper projector_helper_; std::unique_ptr window_; base::HistogramTester histogram_tester_; }; // static constexpr gfx::Rect ProjectorCaptureModeIntegrationTests::kUserRegion; TEST_F(ProjectorCaptureModeIntegrationTests, EntryPoint) { // With the most recent source type set to kImage, starting capture mode for // the projector workflow will still force it to kVideo. auto* controller = CaptureModeController::Get(); controller->SetType(CaptureModeType::kImage); // Also, audio recording is initially disabled. However, the projector flow // forces it enabled. EXPECT_FALSE(controller->enable_audio_recording()); StartProjectorModeSession(); EXPECT_TRUE(controller->IsActive()); auto* session = controller->capture_mode_session(); EXPECT_TRUE(session->is_in_projector_mode()); EXPECT_TRUE(controller->enable_audio_recording()); constexpr char kEntryPointHistogram[] = "Ash.CaptureModeController.EntryPoint.ClamshellMode"; histogram_tester_.ExpectBucketCount(kEntryPointHistogram, CaptureModeEntryType::kProjector, 1); } // Tests that the settings view is simplified in projector mode. TEST_F(ProjectorCaptureModeIntegrationTests, CaptureModeSettings) { auto* controller = CaptureModeController::Get(); StartProjectorModeSession(); auto* event_generator = GetEventGenerator(); ClickOnView(GetSettingsButton(), event_generator); CaptureModeSettingsTestApi test_api; // The "Save-to" menu group should never be added. CaptureModeMenuGroup* save_to_menu_group = test_api.GetSaveToMenuGroup(); EXPECT_FALSE(save_to_menu_group); // The audio-off option should never be added. EXPECT_FALSE(test_api.GetAudioOffOption()); CaptureModeMenuGroup* audio_input_menu_group = test_api.GetAudioInputMenuGroup(); EXPECT_TRUE(audio_input_menu_group->IsOptionChecked(kAudioMicrophone)); EXPECT_TRUE(controller->enable_audio_recording()); } // Tests the keyboard navigation for projector mode. The `image_toggle_button_` // in `CaptureModeTypeView` and the `Off` audio input option in // `CaptureModeSettingsView` are not available in projector mode. TEST_F(ProjectorCaptureModeIntegrationTests, KeyboardNavigationBasic) { auto* controller = CaptureModeController::Get(); // Use `kFullscreen` here to minimize the number of tabs to reach the setting // button. controller->SetSource(CaptureModeSource::kFullscreen); StartProjectorModeSession(); using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; CaptureModeSessionTestApi test_api(controller->capture_mode_session()); EXPECT_FALSE(GetImageToggleButton()); // Tab once, check the current focused view is the video toggle button. auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(test_api.GetCurrentFocusedView()->GetView(), GetVideoToggleButton()); // Now tab four times to focus the settings button and enter space to open the // setting menu. SendKey(ui::VKEY_TAB, event_generator, /*shift_down=*/false, /*count=*/4); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(FocusGroup::kPendingSettings, test_api.GetCurrentFocusGroup()); CaptureModeSettingsView* settings_menu = test_api.GetCaptureModeSettingsView(); ASSERT_TRUE(settings_menu); CaptureModeSettingsTestApi settings_test_api; // The `Off` option should not be visible. EXPECT_FALSE(settings_test_api.GetAudioOffOption()); // Tab twice, the current focused view is the `Microphone` option. SendKey(ui::VKEY_TAB, event_generator, /*shift_down=*/false, /*count=*/2); EXPECT_EQ(test_api.GetCurrentFocusedView()->GetView(), settings_test_api.GetMicrophoneOption()); } TEST_F(ProjectorCaptureModeIntegrationTests, BarButtonsState) { auto* controller = CaptureModeController::Get(); StartProjectorModeSession(); EXPECT_TRUE(controller->IsActive()); // The image toggle button shouldn't be available, whereas the video toggle // button should be enabled and active. EXPECT_FALSE(GetImageToggleButton()); EXPECT_TRUE(GetVideoToggleButton()->GetEnabled()); EXPECT_TRUE(GetVideoToggleButton()->GetToggled()); } TEST_F(ProjectorCaptureModeIntegrationTests, StartEndRecording) { auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kFullscreen); StartProjectorModeSession(); EXPECT_TRUE(controller->IsActive()); histogram_tester_.ExpectUniqueSample(kProjectorCreationFlowHistogramName, ProjectorCreationFlow::kSessionStarted, /*count=*/1); // Hit Enter to begin recording. The recording session should be marked for // projector. PressAndReleaseKey(ui::VKEY_RETURN); EXPECT_CALL(*projector_client(), StartSpeechRecognition()); WaitForRecordingToStart(); histogram_tester_.ExpectBucketCount(kProjectorCreationFlowHistogramName, ProjectorCreationFlow::kRecordingStarted, /*count=*/1); EXPECT_FALSE(controller->IsActive()); EXPECT_TRUE(controller->is_recording_in_progress()); EXPECT_TRUE(controller->video_recording_watcher_for_testing() ->is_in_projector_mode()); EXPECT_CALL(*projector_client(), StopSpeechRecognition()); controller->EndVideoRecording(EndRecordingReason::kStopRecordingButton); WaitForCaptureFileToBeSaved(); histogram_tester_.ExpectBucketCount(kProjectorCreationFlowHistogramName, ProjectorCreationFlow::kRecordingEnded, /*count=*/1); histogram_tester_.ExpectBucketCount(kProjectorCreationFlowHistogramName, ProjectorCreationFlow::kSessionStopped, /*count=*/1); histogram_tester_.ExpectTotalCount(kProjectorCreationFlowHistogramName, /*count=*/4); } TEST_F(ProjectorCaptureModeIntegrationTests, ProjectorSessionNeverStartsWhenCaptureModeIsBlocked) { auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kFullscreen); auto* test_delegate = static_cast(controller->delegate_for_testing()); test_delegate->set_is_allowed_by_policy(false); ProjectorController::Get()->StartProjectorSession("projector_data"); // Both sessions will never start. EXPECT_FALSE(controller->IsActive()); EXPECT_FALSE(ProjectorSession::Get()->is_active()); EXPECT_FALSE(controller->is_recording_in_progress()); } TEST_F(ProjectorCaptureModeIntegrationTests, ProjectorSessionNeverStartsWhenVideoRecordingIsOnGoing) { auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); EXPECT_CALL( *projector_client(), OnNewScreencastPreconditionChanged(NewScreencastPrecondition( NewScreencastPreconditionState::kDisabled, {NewScreencastPreconditionReason::kScreenRecordingInProgress}))); controller->StartVideoRecordingImmediatelyForTesting(); EXPECT_TRUE(controller->is_recording_in_progress()); EXPECT_FALSE(ProjectorSession::Get()->is_active()); EXPECT_NE(ProjectorController::Get()->GetNewScreencastPrecondition().state, NewScreencastPreconditionState::kEnabled); // There is another OnNewScreencastPreconditionChanged() call during tear // down. EXPECT_CALL(*projector_client(), OnNewScreencastPreconditionChanged(NewScreencastPrecondition( NewScreencastPreconditionState::kEnabled, {}))); } namespace { enum AbortReason { kBlockedByDlp, kBlockedByPolicy, kUserPressedEsc, }; struct { const std::string scope_trace; const AbortReason reason; } kTestCases[] = { {"Blocked by DLP", kBlockedByDlp}, {"Blocked by policy", kBlockedByPolicy}, {"User Pressed Esc", kUserPressedEsc}, }; } // namespace TEST_F(ProjectorCaptureModeIntegrationTests, ProjectorSessionAbortedBeforeCountDownStarts) { auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kFullscreen); for (const auto& test_case : kTestCases) { SCOPED_TRACE(test_case.scope_trace); StartProjectorModeSession(); auto* test_delegate = static_cast( controller->delegate_for_testing()); switch (test_case.reason) { case kBlockedByDlp: test_delegate->set_is_allowed_by_dlp(false); PressAndReleaseKey(ui::VKEY_RETURN); break; case kBlockedByPolicy: test_delegate->set_is_allowed_by_policy(false); PressAndReleaseKey(ui::VKEY_RETURN); break; case kUserPressedEsc: PressAndReleaseKey(ui::VKEY_ESCAPE); break; } // The session will end immediately without a count down. EXPECT_FALSE(controller->IsActive()); EXPECT_FALSE(ProjectorSession::Get()->is_active()); EXPECT_FALSE(controller->is_recording_in_progress()); // Prepare for next iteration by resetting things back to default. test_delegate->ResetAllowancesToDefault(); } histogram_tester_.ExpectBucketCount(kProjectorCreationFlowHistogramName, ProjectorCreationFlow::kSessionStarted, /*count=*/3); histogram_tester_.ExpectBucketCount(kProjectorCreationFlowHistogramName, ProjectorCreationFlow::kRecordingAborted, /*count=*/3); histogram_tester_.ExpectBucketCount(kProjectorCreationFlowHistogramName, ProjectorCreationFlow::kSessionStopped, /*count=*/3); histogram_tester_.ExpectTotalCount(kProjectorCreationFlowHistogramName, /*count=*/9); } TEST_F(ProjectorCaptureModeIntegrationTests, ProjectorSessionAbortedAfterCountDownStarts) { ui::ScopedAnimationDurationScaleMode animation_scale( ui::ScopedAnimationDurationScaleMode::FAST_DURATION); auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kFullscreen); for (const auto& test_case : kTestCases) { SCOPED_TRACE(test_case.scope_trace); StartProjectorModeSession(); PressAndReleaseKey(ui::VKEY_RETURN); auto* test_delegate = static_cast( controller->delegate_for_testing()); switch (test_case.reason) { case kBlockedByDlp: test_delegate->set_is_allowed_by_dlp(false); break; case kBlockedByPolicy: test_delegate->set_is_allowed_by_policy(false); break; case kUserPressedEsc: PressAndReleaseKey(ui::VKEY_ESCAPE); break; } WaitForSessionToEnd(); EXPECT_FALSE(ProjectorSession::Get()->is_active()); EXPECT_FALSE(controller->is_recording_in_progress()); // Prepare for next iteration by resetting things back to default. test_delegate->ResetAllowancesToDefault(); } histogram_tester_.ExpectBucketCount(kProjectorCreationFlowHistogramName, ProjectorCreationFlow::kSessionStarted, /*count=*/3); histogram_tester_.ExpectBucketCount(kProjectorCreationFlowHistogramName, ProjectorCreationFlow::kRecordingAborted, /*count=*/3); histogram_tester_.ExpectBucketCount(kProjectorCreationFlowHistogramName, ProjectorCreationFlow::kSessionStopped, /*count=*/3); histogram_tester_.ExpectTotalCount(kProjectorCreationFlowHistogramName, /*count=*/9); } TEST_F(ProjectorCaptureModeIntegrationTests, RecordingOverlayWidget) { auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kFullscreen); StartProjectorModeSession(); EXPECT_TRUE(controller->IsActive()); PressAndReleaseKey(ui::VKEY_RETURN); WaitForRecordingToStart(); CaptureModeTestApi test_api; RecordingOverlayController* overlay_controller = test_api.GetRecordingOverlayController(); EXPECT_FALSE(overlay_controller->is_enabled()); auto* overlay_window = overlay_controller->GetOverlayNativeWindow(); VerifyOverlayEnabledState(overlay_window, /*overlay_enabled_state=*/false); auto* projector_controller = ProjectorControllerImpl::Get(); projector_controller->EnableAnnotatorTool(); EXPECT_TRUE(overlay_controller->is_enabled()); VerifyOverlayEnabledState(overlay_window, /*overlay_enabled_state=*/true); projector_controller->ResetTools(); EXPECT_FALSE(overlay_controller->is_enabled()); VerifyOverlayEnabledState(overlay_window, /*overlay_enabled_state=*/false); } TEST_F(ProjectorCaptureModeIntegrationTests, RecordingOverlayDockedMagnifier) { auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kFullscreen); StartProjectorModeSession(); EXPECT_TRUE(controller->IsActive()); PressAndReleaseKey(ui::VKEY_RETURN); WaitForRecordingToStart(); CaptureModeTestApi test_api; RecordingOverlayController* overlay_controller = test_api.GetRecordingOverlayController(); auto* projector_controller = ProjectorControllerImpl::Get(); projector_controller->EnableAnnotatorTool(); EXPECT_TRUE(overlay_controller->is_enabled()); auto* overlay_window = overlay_controller->GetOverlayNativeWindow(); // Before the docked magnifier gets enabled, the overlay's bounds should match // the root window's bounds. auto* root_window = overlay_window->GetRootWindow(); const gfx::Rect root_window_bounds = root_window->bounds(); EXPECT_EQ(root_window_bounds, overlay_window->GetBoundsInRootWindow()); // Once the magnifier is enabled, the overlay should be pushed down so that // it doesn't cover the magnifier viewport. auto* docked_magnifier = Shell::Get()->docked_magnifier_controller(); docked_magnifier->SetEnabled(true); const gfx::Rect expected_bounds = gfx::SubtractRects( root_window_bounds, docked_magnifier->GetTotalMagnifierBoundsForRoot(root_window)); EXPECT_EQ(expected_bounds, overlay_window->GetBoundsInRootWindow()); // It should go back to original bounds once the magnifier is disabled. docked_magnifier->SetEnabled(false); EXPECT_EQ(root_window_bounds, overlay_window->GetBoundsInRootWindow()); } TEST_P(ProjectorCaptureModeIntegrationTests, RecordingOverlayWidgetBounds) { const auto capture_source = GetParam(); StartRecordingForProjectorFromSource(capture_source); CaptureModeTestApi test_api; RecordingOverlayController* overlay_controller = test_api.GetRecordingOverlayController(); EXPECT_FALSE(overlay_controller->is_enabled()); auto* overlay_window = overlay_controller->GetOverlayNativeWindow(); VerifyOverlayWindow(overlay_window, capture_source); } // Regression test for https://crbug.com/1322655. TEST_P(ProjectorCaptureModeIntegrationTests, RecordingOverlayWidgetBoundsSecondDisplay) { UpdateDisplay("800x700,801+0-800x700"); const gfx::Point point_in_second_display = gfx::Point(1000, 500); auto* event_generator = GetEventGenerator(); MoveMouseToAndUpdateCursorDisplay(point_in_second_display, event_generator); window()->SetBoundsInScreen( gfx::Rect(900, 0, 600, 500), display::Screen::GetScreen()->GetDisplayNearestWindow( Shell::GetAllRootWindows()[1])); const auto capture_source = GetParam(); StartRecordingForProjectorFromSource(capture_source); const auto roots = Shell::GetAllRootWindows(); EXPECT_EQ(roots[1], GetWindowBeingRecorded()->GetRootWindow()); CaptureModeTestApi test_api; RecordingOverlayController* overlay_controller = test_api.GetRecordingOverlayController(); EXPECT_FALSE(overlay_controller->is_enabled()); auto* overlay_window = overlay_controller->GetOverlayNativeWindow(); VerifyOverlayWindow(overlay_window, capture_source); } namespace { // Defines a class that intercepts the events at the post-target handling phase // and caches the last event target to which the event was routed. class EventTargetCatcher : public ui::EventHandler { public: EventTargetCatcher() { Shell::GetPrimaryRootWindow()->AddPostTargetHandler(this); } EventTargetCatcher(const EventTargetCatcher&) = delete; EventTargetCatcher& operator=(const EventTargetCatcher&) = delete; ~EventTargetCatcher() override { Shell::GetPrimaryRootWindow()->RemovePostTargetHandler(this); } ui::EventTarget* last_event_target() { return last_event_target_; } // ui::EventHandler: void OnEvent(ui::Event* event) override { ui::EventHandler::OnEvent(event); last_event_target_ = event->target(); } private: ui::EventTarget* last_event_target_ = nullptr; }; } // namespace TEST_F(ProjectorCaptureModeIntegrationTests, RecordingOverlayWidgetTargeting) { auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kFullscreen); StartProjectorModeSession(); EXPECT_TRUE(controller->IsActive()); PressAndReleaseKey(ui::VKEY_RETURN); WaitForRecordingToStart(); CaptureModeTestApi test_api; RecordingOverlayController* overlay_controller = test_api.GetRecordingOverlayController(); auto* projector_controller = ProjectorControllerImpl::Get(); projector_controller->EnableAnnotatorTool(); EXPECT_TRUE(overlay_controller->is_enabled()); auto* overlay_window = overlay_controller->GetOverlayNativeWindow(); VerifyOverlayEnabledState(overlay_window, /*overlay_enabled_state=*/true); // Open the annotation tray bubble. auto* root_window = Shell::GetPrimaryRootWindow(); auto* status_area_widget = RootWindowController::ForWindow(root_window)->GetStatusAreaWidget(); ProjectorAnnotationTray* annotations_tray = status_area_widget->projector_annotation_tray(); annotations_tray->ShowBubble(); EXPECT_TRUE(annotations_tray->GetBubbleView()); // Clicking anywhere outside the projector shelf pod should be targeted to the // overlay widget window and close the annotation tray bubble. EventTargetCatcher event_target_catcher; auto* event_generator = GetEventGenerator(); event_generator->set_current_screen_location(gfx::Point(10, 10)); event_generator->ClickLeftButton(); EXPECT_EQ(overlay_window, event_target_catcher.last_event_target()); EXPECT_FALSE(annotations_tray->GetBubbleView()); // Now move the mouse over the projector shelf pod, the overlay should not // consume the event, and it should instead go through to that pod. EXPECT_TRUE(annotations_tray->visible_preferred()); event_generator->MoveMouseTo( annotations_tray->GetBoundsInScreen().CenterPoint()); EXPECT_EQ(annotations_tray->GetWidget()->GetNativeWindow(), event_target_catcher.last_event_target()); // The overlay status hasn't changed. VerifyOverlayEnabledState(overlay_window, /*overlay_enabled_state=*/true); // Now move the mouse and then click on the stop recording button, the overlay // should not consume the event. The video recording should be ended. StopRecordingButtonTray* stop_recording_button = status_area_widget->stop_recording_button_tray(); const gfx::Point stop_button_center_point = stop_recording_button->GetBoundsInScreen().CenterPoint(); event_generator->MoveMouseTo(stop_button_center_point); event_generator->ClickLeftButton(); EXPECT_FALSE(controller->is_recording_in_progress()); } // Tests that auto hidden shelf can be brought back if user moves mouse to the // shelf activation area even while annotation is active. TEST_F(ProjectorCaptureModeIntegrationTests, BringBackAutoHiddenShelfWhileAnnotationIsOn) { auto* root_window = Shell::GetPrimaryRootWindow(); // Set `shelf` to always auto-hidden. Shelf* shelf = RootWindowController::ForWindow(root_window)->shelf(); shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kAlways); auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kFullscreen); StartProjectorModeSession(); EXPECT_TRUE(controller->IsActive()); PressAndReleaseKey(ui::VKEY_RETURN); WaitForRecordingToStart(); auto* event_generator = GetEventGenerator(); auto* projector_controller = ProjectorControllerImpl::Get(); const gfx::Rect root_window_bounds_in_screen = root_window->GetBoundsInScreen(); const int display_width = root_window_bounds_in_screen.width(); const int display_height = root_window_bounds_in_screen.height(); const gfx::Point display_center = root_window_bounds_in_screen.CenterPoint(); struct { const std::string scope_trace; const ShelfAlignment shelf_alignment; } kTestCases[] = { {"Shelf has botton alignment", ShelfAlignment::kBottom}, {"Shelf has left alignment", ShelfAlignment::kLeft}, {"Shelf has right alignment", ShelfAlignment::kRight}, }; for (const auto& test_case : kTestCases) { SCOPED_TRACE(test_case.scope_trace); // Enable annotation. projector_controller->EnableAnnotatorTool(); // Verify shelf is invisible right now. EXPECT_FALSE(shelf->IsVisible()); shelf->SetAlignment(test_case.shelf_alignment); switch (test_case.shelf_alignment) { case ShelfAlignment::kBottom: case ShelfAlignment::kBottomLocked: event_generator->MoveMouseTo(0, display_height); break; case ShelfAlignment::kLeft: event_generator->MoveMouseTo(0, display_height); break; case ShelfAlignment::kRight: event_generator->MoveMouseTo(display_width, display_height); break; } // Verify after mouse is moved on top of the shelf activation area, shelf is // brought back and visible once the animation to show shelf is finished. ShellTestApi().WaitForWindowFinishAnimating(shelf->GetWindow()); EXPECT_TRUE(shelf->IsVisible()); // Disable annotation. projector_controller->ResetTools(); // Move mouse to the outside of the shelf activation area, and wait for the // animation to hide shelf to finish. event_generator->MoveMouseTo(display_center); ShellTestApi().WaitForWindowFinishAnimating(shelf->GetWindow()); } } // Tests that neither preview notification nor recording in tote is shown if in // projector mode. TEST_P(ProjectorCaptureModeIntegrationTests, NotShowRecordingInToteOrNotificationForProjectorMode) { const auto capture_source = GetParam(); StartRecordingForProjectorFromSource(capture_source); CaptureModeTestApi().StopVideoRecording(); WaitForCaptureFileToBeSaved(); EXPECT_FALSE(GetPreviewNotification()); ash::HoldingSpaceTestApi holding_space_api; EXPECT_TRUE(holding_space_api.GetScreenCaptureViews().empty()); } // Tests that metrics are recorded correctly for capture configuration entering // from projector in both clamshell and tablet mode. TEST_P(ProjectorCaptureModeIntegrationTests, ProjectorCaptureConfigurationMetrics) { const auto capture_source = GetParam(); constexpr char kProjectorCaptureConfigurationHistogramBase[] = "Ash.CaptureModeController.Projector.CaptureConfiguration"; ash::CaptureModeTestApi test_api; const bool kTabletEnabledStates[]{false, true}; for (const bool tablet_enabled : kTabletEnabledStates) { if (tablet_enabled) { SwitchToTabletMode(); EXPECT_TRUE(Shell::Get()->IsInTabletMode()); } else { EXPECT_FALSE(Shell::Get()->IsInTabletMode()); } histogram_tester_.ExpectBucketCount( GetCaptureModeHistogramName( kProjectorCaptureConfigurationHistogramBase), GetConfiguration(CaptureModeType::kVideo, capture_source), 0); StartRecordingForProjectorFromSource(capture_source); WaitForSeconds(1); test_api.StopVideoRecording(); EXPECT_FALSE(CaptureModeController::Get()->is_recording_in_progress()); histogram_tester_.ExpectUniqueSample( GetCaptureModeHistogramName( kProjectorCaptureConfigurationHistogramBase), GetConfiguration(CaptureModeType::kVideo, capture_source), 1); WaitForCaptureFileToBeSaved(); } } // Tests that metrics are recorded correctly for screen recording length // entering from projector in both clamshell and tablet mode. TEST_P(ProjectorCaptureModeIntegrationTests, ProjectorScreenRecordingLengthMetrics) { const auto capture_source = GetParam(); constexpr char kProjectorRecordTimeHistogramBase[] = "Ash.CaptureModeController.Projector.ScreenRecordingLength"; ash::CaptureModeTestApi test_api; const bool kTabletEnabledStates[]{false, true}; for (const bool tablet_enabled : kTabletEnabledStates) { if (tablet_enabled) { SwitchToTabletMode(); EXPECT_TRUE(Shell::Get()->IsInTabletMode()); } else { EXPECT_FALSE(Shell::Get()->IsInTabletMode()); } StartRecordingForProjectorFromSource(capture_source); WaitForSeconds(5); test_api.StopVideoRecording(); EXPECT_FALSE(CaptureModeController::Get()->is_recording_in_progress()); WaitForCaptureFileToBeSaved(); histogram_tester_.ExpectUniqueSample( GetCaptureModeHistogramName(kProjectorRecordTimeHistogramBase), /*seconds=*/5, /*count=*/1); } } // Tests that metrics are recorded correctly for capture region adjustment // entering from projector in both clamshell and tablet mode. TEST_F(ProjectorCaptureModeIntegrationTests, ProjectorCaptureRegionAdjustmentTest) { constexpr char kProjectorCaptureRegionAdjustmentHistogramBase[] = "Ash.CaptureModeController.Projector.CaptureRegionAdjusted"; auto histogram_name = GetCaptureModeHistogramName( kProjectorCaptureRegionAdjustmentHistogramBase); histogram_tester_.ExpectBucketCount( GetCaptureModeHistogramName( kProjectorCaptureRegionAdjustmentHistogramBase), 0, 0); auto resize_and_reset_region = [](ui::test::EventGenerator* event_generator, const gfx::Point& top_right) { // Enlarges the region and then resize it back to its original size. event_generator->set_current_screen_location(top_right); event_generator->DragMouseTo(top_right + gfx::Vector2d(50, 50)); event_generator->DragMouseTo(top_right); }; auto move_and_reset_region = [](ui::test::EventGenerator* event_generator, const gfx::Point& drag_point) { // Moves the region and then moves it back to its original position. event_generator->set_current_screen_location(drag_point); event_generator->DragMouseTo(drag_point + gfx::Vector2d(-50, -50)); event_generator->DragMouseTo(drag_point); }; ash::CaptureModeTestApi test_api; auto* event_generator = GetEventGenerator(); const gfx::Rect target_region(gfx::Rect(100, 100, 200, 200)); auto top_right = target_region.top_right(); const bool kTabletEnabledStates[] = {false, true}; for (const bool tablet_enabled : kTabletEnabledStates) { if (tablet_enabled) { SwitchToTabletMode(); EXPECT_TRUE(Shell::Get()->IsInTabletMode()); } else { EXPECT_FALSE(Shell::Get()->IsInTabletMode()); } StartProjectorModeSession(); auto* controller = CaptureModeController::Get(); controller->SetSource(CaptureModeSource::kRegion); test_api.SetUserSelectedRegion(target_region); // Resize the region twice by dragging the top right of the region out and // then back again. resize_and_reset_region(event_generator, top_right); // Move the region twice by dragging within the region. const gfx::Point drag_point(300, 300); move_and_reset_region(event_generator, drag_point); test_api.PerformCapture(); WaitForSeconds(1); test_api.StopVideoRecording(); EXPECT_FALSE(controller->is_recording_in_progress()); histogram_tester_.ExpectBucketCount( GetCaptureModeHistogramName( kProjectorCaptureRegionAdjustmentHistogramBase), 4, 1); WaitForCaptureFileToBeSaved(); } } INSTANTIATE_TEST_SUITE_P(All, ProjectorCaptureModeIntegrationTests, testing::Values(CaptureModeSource::kFullscreen, CaptureModeSource::kRegion, CaptureModeSource::kWindow)); // ----------------------------------------------------------------------------- // CaptureModeSettingsTest: // Test fixture for CaptureMode settings view. class CaptureModeSettingsTest : public CaptureModeTest { public: CaptureModeSettingsTest() = default; ~CaptureModeSettingsTest() override = default; // CaptureModeTest: void SetUp() override { CaptureModeTest::SetUp(); FakeFolderSelectionDialogFactory::Start(); } void TearDown() override { FakeFolderSelectionDialogFactory::Stop(); CaptureModeTest::TearDown(); } CaptureModeSettingsView* GetCaptureModeSettingsView() const { auto* session = CaptureModeController::Get()->capture_mode_session(); DCHECK(session); return CaptureModeSessionTestApi(session).GetCaptureModeSettingsView(); } void WaitForSettingsMenuToBeRefreshed() { base::RunLoop run_loop; CaptureModeSettingsTestApi().SetOnSettingsMenuRefreshedCallback( run_loop.QuitClosure()); run_loop.Run(); } }; enum class NudgeDismissalCause { kPressSettingsButton, kCaptureViaEnterKey, kCaptureViaClickOnScreen, kCaptureViaLabelButton, }; // Test fixture to test that various causes that lead to the dismissal of the // user nudge, they dismiss it forever. class CaptureModeNudgeDismissalTest : public CaptureModeSettingsTest, public ::testing::WithParamInterface { public: // Starts a session appropriate for the test param. CaptureModeController* StartSession() { switch (GetParam()) { case NudgeDismissalCause::kPressSettingsButton: case NudgeDismissalCause::kCaptureViaEnterKey: case NudgeDismissalCause::kCaptureViaClickOnScreen: return StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); case NudgeDismissalCause::kCaptureViaLabelButton: auto* controller = CaptureModeController::Get(); controller->SetUserCaptureRegion(gfx::Rect(200, 300), /*by_user=*/true); StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kImage); return controller; } } void DoDismissalAction() { auto* controller = CaptureModeController::Get(); auto* event_generator = GetEventGenerator(); switch (GetParam()) { case NudgeDismissalCause::kPressSettingsButton: ClickOnView(GetSettingsButton(), event_generator); break; case NudgeDismissalCause::kCaptureViaEnterKey: PressAndReleaseKey(ui::VKEY_RETURN); EXPECT_FALSE(controller->IsActive()); break; case NudgeDismissalCause::kCaptureViaClickOnScreen: event_generator->MoveMouseToCenterOf(Shell::GetPrimaryRootWindow()); event_generator->ClickLeftButton(); EXPECT_FALSE(controller->IsActive()); break; case NudgeDismissalCause::kCaptureViaLabelButton: auto* label_button_widget = CaptureModeSessionTestApi(controller->capture_mode_session()) .GetCaptureLabelWidget(); EXPECT_TRUE(label_button_widget); ClickOnView(label_button_widget->GetContentsView(), event_generator); break; } } }; TEST_P(CaptureModeNudgeDismissalTest, NudgeDismissedForever) { auto* controller = StartSession(); auto* capture_session = controller->capture_mode_session(); auto* capture_toast_controller = capture_session->capture_toast_controller(); auto* nudge_controller = GetUserNudgeController(); ASSERT_TRUE(nudge_controller); EXPECT_TRUE(nudge_controller->is_visible()); EXPECT_TRUE(capture_toast_controller->capture_toast_widget()); ASSERT_TRUE(capture_toast_controller->current_toast_type()); EXPECT_EQ(*(capture_toast_controller->current_toast_type()), CaptureToastType::kUserNudge); // Trigger the action that dismisses the nudge forever, it should be removed // in this session (if the action doesn't stop the session) and any future // sessions. DoDismissalAction(); if (controller->IsActive()) { EXPECT_FALSE(GetUserNudgeController()); // Close the session in preparation for opening a new one. controller->Stop(); } // Reopen a new session, the nudge should not show anymore. StartImageRegionCapture(); EXPECT_FALSE(GetUserNudgeController()); } INSTANTIATE_TEST_SUITE_P( All, CaptureModeNudgeDismissalTest, testing::Values(NudgeDismissalCause::kPressSettingsButton, NudgeDismissalCause::kCaptureViaEnterKey, NudgeDismissalCause::kCaptureViaClickOnScreen, NudgeDismissalCause::kCaptureViaLabelButton)); TEST_F(CaptureModeSettingsTest, NudgeChangesRootWithBar) { UpdateDisplay("800x700,801+0-800x700"); auto* event_generator = GetEventGenerator(); MoveMouseToAndUpdateCursorDisplay(gfx::Point(100, 500), event_generator); auto* controller = StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kImage); auto* session = controller->capture_mode_session(); auto* capture_toast_controller = session->capture_toast_controller(); EXPECT_EQ(Shell::GetAllRootWindows()[0], session->current_root()); EXPECT_EQ(capture_toast_controller->capture_toast_widget() ->GetNativeWindow() ->GetRootWindow(), session->current_root()); MoveMouseToAndUpdateCursorDisplay(gfx::Point(1000, 500), event_generator); EXPECT_EQ(Shell::GetAllRootWindows()[1], session->current_root()); EXPECT_EQ(capture_toast_controller->capture_toast_widget() ->GetNativeWindow() ->GetRootWindow(), session->current_root()); } TEST_F(CaptureModeSettingsTest, NudgeBehaviorWhenSelectingRegion) { UpdateDisplay("800x700,801+0-800x700"); auto* event_generator = GetEventGenerator(); MoveMouseToAndUpdateCursorDisplay(gfx::Point(100, 500), event_generator); auto* controller = StartImageRegionCapture(); auto* session = controller->capture_mode_session(); EXPECT_EQ(Shell::GetAllRootWindows()[0], session->current_root()); // Nudge hides while selecting a region, but doesn't change roots until the // region change is committed. auto* nudge_controller = GetUserNudgeController(); MoveMouseToAndUpdateCursorDisplay(gfx::Point(1000, 500), event_generator); event_generator->PressLeftButton(); EXPECT_FALSE(nudge_controller->is_visible()); event_generator->MoveMouseBy(50, 60); event_generator->ReleaseLeftButton(); EXPECT_EQ(Shell::GetAllRootWindows()[1], session->current_root()); // The nudge shows again, and is on the second display. EXPECT_TRUE(nudge_controller->is_visible()); EXPECT_EQ(session->capture_toast_controller() ->capture_toast_widget() ->GetNativeWindow() ->GetRootWindow(), session->current_root()); } TEST_F(CaptureModeSettingsTest, NudgeDoesNotShowForAllUserTypes) { struct { std::string trace; user_manager::UserType user_type; bool can_see_nudge; } kTestCases[] = { {"regular user", user_manager::USER_TYPE_REGULAR, true}, {"child", user_manager::USER_TYPE_CHILD, true}, {"guest", user_manager::USER_TYPE_GUEST, false}, {"public account", user_manager::USER_TYPE_PUBLIC_ACCOUNT, false}, {"kiosk app", user_manager::USER_TYPE_KIOSK_APP, false}, {"arc kiosk app", user_manager::USER_TYPE_ARC_KIOSK_APP, false}, {"web kiosk app", user_manager::USER_TYPE_WEB_KIOSK_APP, false}, {"active dir", user_manager::USER_TYPE_ACTIVE_DIRECTORY, false}, }; for (const auto& test_case : kTestCases) { SCOPED_TRACE(test_case.trace); ClearLogin(); SimulateUserLogin("example@gmail.com", test_case.user_type); auto* controller = StartImageRegionCapture(); EXPECT_EQ(test_case.can_see_nudge, controller->CanShowUserNudge()); auto* nudge_controller = GetUserNudgeController(); EXPECT_EQ(test_case.can_see_nudge, !!nudge_controller); controller->Stop(); } } // Tests that it's possbile to take a screenshot using the keyboard shortcut at // the login screen without any crashes. https://crbug.com/1266728. TEST_F(CaptureModeSettingsTest, TakeScreenshotAtLoginScreen) { ClearLogin(); PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1, ui::EF_CONTROL_DOWN); WaitForCaptureFileToBeSaved(); } // Tests that clicking on audio input buttons updates the state in the // controller, and persists between sessions. TEST_F(CaptureModeSettingsTest, AudioInputSettingsMenu) { auto* controller = StartImageRegionCapture(); auto* event_generator = GetEventGenerator(); // Test that the audio recording preference is defaulted to off. ClickOnView(GetSettingsButton(), event_generator); EXPECT_FALSE(controller->enable_audio_recording()); CaptureModeSettingsTestApi test_api; CaptureModeMenuGroup* audio_input_menu_group = test_api.GetAudioInputMenuGroup(); EXPECT_TRUE(audio_input_menu_group->IsOptionChecked(kAudioOff)); EXPECT_FALSE(audio_input_menu_group->IsOptionChecked(kAudioMicrophone)); // Click on the |microphone| option. It should be checked after click along // with |off| is unchecked. Recording preference is set to microphone. views::View* microphone_option = test_api.GetMicrophoneOption(); ClickOnView(microphone_option, event_generator); EXPECT_TRUE(audio_input_menu_group->IsOptionChecked(kAudioMicrophone)); EXPECT_FALSE(audio_input_menu_group->IsOptionChecked(kAudioOff)); EXPECT_TRUE(controller->enable_audio_recording()); // Test that the user selected audio preference for audio recording is // remembered between sessions. SendKey(ui::VKEY_ESCAPE, event_generator); StartImageRegionCapture(); EXPECT_TRUE(controller->enable_audio_recording()); } TEST_F(CaptureModeSettingsTest, SelectFolderFromDialog) { auto* controller = StartImageRegionCapture(); auto* event_generator = GetEventGenerator(); ClickOnView(GetSettingsButton(), event_generator); // Initially there should only be an option for the default downloads folder. CaptureModeSettingsTestApi test_api; EXPECT_TRUE(test_api.GetDefaultDownloadsOption()); EXPECT_FALSE(test_api.GetCustomFolderOptionIfAny()); CaptureModeMenuGroup* save_to_menu_group = test_api.GetSaveToMenuGroup(); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); ClickOnView(test_api.GetSelectFolderMenuItem(), event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); EXPECT_FALSE(IsAllCaptureSessionUisVisible()); auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); auto* dialog_window = dialog_factory->GetDialogWindow(); auto* window_state = WindowState::Get(dialog_window); ASSERT_TRUE(window_state); EXPECT_FALSE(window_state->CanMaximize()); EXPECT_FALSE(window_state->CanMinimize()); EXPECT_FALSE(window_state->CanResize()); // Accepting the dialog with a folder selection should dismiss it and add a // new option for the custom selected folder in the settings menu. const base::FilePath custom_folder( CreateCustomFolderInUserDownloadsPath("test")); dialog_factory->AcceptPath(custom_folder); WaitForSettingsMenuToBeRefreshed(); EXPECT_FALSE(IsFolderSelectionDialogShown()); EXPECT_TRUE(IsAllCaptureSessionUisVisible()); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kCustomFolder)); EXPECT_FALSE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); EXPECT_EQ(u"test", save_to_menu_group->GetOptionLabelForTesting(kCustomFolder)); // This should update the folder that will be used by the controller. auto capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_EQ(capture_folder.path, custom_folder); EXPECT_FALSE(capture_folder.is_default_downloads_folder); } // Tests that folder selection dialog can be opened without crash while in // window capture mode. TEST_F(CaptureModeSettingsTest, SelectFolderInWindowCaptureMode) { std::unique_ptr window1( CreateTestWindow(gfx::Rect(0, 0, 200, 300))); StartCaptureSession(CaptureModeSource::kWindow, CaptureModeType::kImage); auto* event_generator = GetEventGenerator(); ClickOnView(GetSettingsButton(), event_generator); CaptureModeSettingsTestApi test_api; ClickOnView(test_api.GetSelectFolderMenuItem(), event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); } TEST_F(CaptureModeSettingsTest, DismissDialogWithoutSelection) { auto* controller = StartImageRegionCapture(); const auto old_capture_folder = controller->GetCurrentCaptureFolder(); // Open the settings menu, and click the "Select folder" menu item. auto* event_generator = GetEventGenerator(); ClickOnView(GetSettingsButton(), event_generator); CaptureModeSettingsTestApi test_api; ClickOnView(test_api.GetSelectFolderMenuItem(), event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); // Cancel and dismiss the dialog. There should be no change in the folder // selection. auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); dialog_factory->CancelDialog(); EXPECT_FALSE(IsFolderSelectionDialogShown()); EXPECT_FALSE(test_api.GetCustomFolderOptionIfAny()); CaptureModeMenuGroup* save_to_menu_group = test_api.GetSaveToMenuGroup(); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); const auto new_capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_EQ(old_capture_folder.path, new_capture_folder.path); EXPECT_EQ(old_capture_folder.is_default_downloads_folder, new_capture_folder.is_default_downloads_folder); } TEST_F(CaptureModeSettingsTest, AcceptUpdatedCustomFolderFromDialog) { // Begin a new session with a pre-configured custom folder. auto* controller = CaptureModeController::Get(); const base::FilePath custom_folder( CreateCustomFolderInUserDownloadsPath("test")); controller->SetCustomCaptureFolder(custom_folder); StartImageRegionCapture(); // Open the settings menu and check there already exists an item for that // pre-configured custom folder. auto* event_generator = GetEventGenerator(); ClickOnView(GetSettingsButton(), event_generator); WaitForSettingsMenuToBeRefreshed(); CaptureModeSettingsTestApi test_api; EXPECT_TRUE(test_api.GetDefaultDownloadsOption()); auto* custom_folder_view = test_api.GetCustomFolderOptionIfAny(); EXPECT_TRUE(custom_folder_view); CaptureModeMenuGroup* save_to_menu_group = test_api.GetSaveToMenuGroup(); EXPECT_FALSE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kCustomFolder)); // Now open the folder selection dialog and select a different folder. The // existing *same* item in the menu should be updated. ClickOnView(test_api.GetSelectFolderMenuItem(), event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); const base::FilePath new_folder( CreateCustomFolderInUserDownloadsPath("test1")); dialog_factory->AcceptPath(new_folder); WaitForSettingsMenuToBeRefreshed(); EXPECT_FALSE(IsFolderSelectionDialogShown()); EXPECT_EQ(custom_folder_view, test_api.GetCustomFolderOptionIfAny()); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kCustomFolder)); EXPECT_FALSE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); EXPECT_EQ(u"test1", save_to_menu_group->GetOptionLabelForTesting(kCustomFolder)); // This should update the folder that will be used by the controller. const auto capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_EQ(capture_folder.path, new_folder); EXPECT_FALSE(capture_folder.is_default_downloads_folder); } TEST_F(CaptureModeSettingsTest, InitializeSettingsViewWithUnavailableCustomFolder) { // Begin a new session with a pre-configured unavailable custom folder. auto* controller = CaptureModeController::Get(); const base::FilePath default_folder = controller->delegate_for_testing()->GetUserDefaultDownloadsFolder(); const base::FilePath custom_folder(FILE_PATH_LITERAL("/home/random")); controller->SetCustomCaptureFolder(custom_folder); StartImageRegionCapture(); // Open the settings menu and check there already exists an item for that // pre-configured custom folder. Since the custom folder is unavailable, the // item should be disabled and dimmed. The item of the default folder should // be checked. auto* event_generator = GetEventGenerator(); ClickOnView(GetSettingsButton(), event_generator); WaitForSettingsMenuToBeRefreshed(); CaptureModeSettingsTestApi test_api; EXPECT_TRUE(test_api.GetDefaultDownloadsOption()); auto* custom_folder_view = test_api.GetCustomFolderOptionIfAny(); EXPECT_TRUE(custom_folder_view); CaptureModeMenuGroup* save_to_menu_group = test_api.GetSaveToMenuGroup(); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); EXPECT_FALSE(save_to_menu_group->IsOptionChecked(kCustomFolder)); EXPECT_FALSE(custom_folder_view->GetEnabled()); EXPECT_EQ(u"random", save_to_menu_group->GetOptionLabelForTesting(kCustomFolder)); // Now open the folder selection dialog and select an available folder. The // item of the custom folder should be checked and enabled. ClickOnView(test_api.GetSelectFolderMenuItem(), event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); const base::FilePath new_folder( CreateCustomFolderInUserDownloadsPath("test")); dialog_factory->AcceptPath(new_folder); WaitForSettingsMenuToBeRefreshed(); EXPECT_EQ(custom_folder_view, test_api.GetCustomFolderOptionIfAny()); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kCustomFolder)); EXPECT_FALSE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); EXPECT_TRUE(custom_folder_view->GetEnabled()); EXPECT_EQ(u"test", save_to_menu_group->GetOptionLabelForTesting(kCustomFolder)); } TEST_F(CaptureModeSettingsTest, DeleteCustomFolderFromDialog) { // Begin a new session with a pre-configured custom folder. auto* controller = CaptureModeController::Get(); const base::FilePath custom_folder( CreateCustomFolderInUserDownloadsPath("test")); controller->SetCustomCaptureFolder(custom_folder); StartImageRegionCapture(); // Open the settings menu and check there exists an item for that custom // folder. And the item is checked to indicate the current folder in use to // save the captured files is the custom folder. auto* event_generator = GetEventGenerator(); ClickOnView(GetSettingsButton(), event_generator); WaitForSettingsMenuToBeRefreshed(); CaptureModeSettingsTestApi test_api; EXPECT_TRUE(test_api.GetDefaultDownloadsOption()); auto* custom_folder_view = test_api.GetCustomFolderOptionIfAny(); EXPECT_TRUE(custom_folder_view); CaptureModeMenuGroup* save_to_menu_group = test_api.GetSaveToMenuGroup(); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kCustomFolder)); // Now open the folder selection dialog and delete the custom folder. Check // the item on the settings menu for custom folder is still there but disabled // and dimmed. The item of the default folder is checked now. ClickOnView(test_api.GetSelectFolderMenuItem(), event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); { base::ScopedAllowBlockingForTesting allow_blocking; const bool result = base::DeleteFile(custom_folder); DCHECK(result); } dialog_factory->CancelDialog(); WaitForSettingsMenuToBeRefreshed(); EXPECT_TRUE(custom_folder_view); EXPECT_FALSE(save_to_menu_group->IsOptionChecked(kCustomFolder)); EXPECT_FALSE(custom_folder_view->GetEnabled()); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); } TEST_F(CaptureModeSettingsTest, AcceptDefaultDownloadsFolderFromDialog) { // Begin a new session with a pre-configured custom folder. auto* controller = CaptureModeController::Get(); controller->SetCustomCaptureFolder( base::FilePath(FILE_PATH_LITERAL("/home/tests/foo"))); StartImageRegionCapture(); auto* event_generator = GetEventGenerator(); ClickOnView(GetSettingsButton(), event_generator); WaitForSettingsMenuToBeRefreshed(); CaptureModeSettingsTestApi test_api; ClickOnView(test_api.GetSelectFolderMenuItem(), event_generator); // Selecting the same folder as the default downloads folder should result in // removing the custom folder option from the menu. auto* test_delegate = controller->delegate_for_testing(); const auto default_downloads_folder = test_delegate->GetUserDefaultDownloadsFolder(); auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); dialog_factory->AcceptPath(default_downloads_folder); EXPECT_FALSE(IsFolderSelectionDialogShown()); EXPECT_TRUE(test_api.GetDefaultDownloadsOption()); EXPECT_FALSE(test_api.GetCustomFolderOptionIfAny()); CaptureModeMenuGroup* save_to_menu_group = test_api.GetSaveToMenuGroup(); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); } TEST_F(CaptureModeSettingsTest, SwitchWhichFolderToUserFromOptions) { // Begin a new session with a pre-configured custom folder. auto* controller = CaptureModeController::Get(); const base::FilePath custom_path( (CreateCustomFolderInUserDownloadsPath("test"))); controller->SetCustomCaptureFolder(custom_path); StartImageRegionCapture(); auto* event_generator = GetEventGenerator(); ClickOnView(GetSettingsButton(), event_generator); WaitForSettingsMenuToBeRefreshed(); // Clicking the "Downloads" option will set it as the folder of choice, but // won't clear the custom folder. CaptureModeSettingsTestApi test_api; ClickOnView(test_api.GetDefaultDownloadsOption(), event_generator); CaptureModeMenuGroup* save_to_menu_group = test_api.GetSaveToMenuGroup(); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); EXPECT_FALSE(save_to_menu_group->IsOptionChecked(kCustomFolder)); const auto default_downloads_folder = controller->delegate_for_testing()->GetUserDefaultDownloadsFolder(); auto capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_EQ(capture_folder.path, default_downloads_folder); EXPECT_TRUE(capture_folder.is_default_downloads_folder); EXPECT_EQ(custom_path, controller->GetCustomCaptureFolder()); // Clicking on the custom folder option will switch back to using it. ClickOnView(test_api.GetCustomFolderOptionIfAny(), event_generator); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kCustomFolder)); EXPECT_FALSE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); capture_folder = controller->GetCurrentCaptureFolder(); EXPECT_EQ(capture_folder.path, custom_path); EXPECT_FALSE(capture_folder.is_default_downloads_folder); } // Tests that when there's no overlap betwwen capture label widget and settings // widget, capture label widget is shown/hidden correctly after open/close the // folder selection window. TEST_F(CaptureModeSettingsTest, CaptureLabelViewNotOverlapsWithSettingsView) { // Update the display size to make sure capture label widget will not // overlap with settings widget UpdateDisplay("1200x1000"); auto* controller = StartImageRegionCapture(); auto* event_generator = GetEventGenerator(); // Tests that the capture label widget doesn't overlap with settings widget. // Both capture label widget and settings widget are visible. views::Widget* capture_label_widget = GetCaptureModeLabelWidget(); ClickOnView(GetSettingsButton(), event_generator); views::Widget* settings_widget = GetCaptureModeSettingsWidget(); EXPECT_FALSE(capture_label_widget->GetWindowBoundsInScreen().Intersects( settings_widget->GetWindowBoundsInScreen())); EXPECT_TRUE(capture_label_widget->IsVisible()); EXPECT_TRUE(settings_widget->IsVisible()); // Open folder selection window, check that both capture label widget and // settings widget are invisible. CaptureModeSettingsTestApi test_api; auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); ClickOnView(test_api.GetSelectFolderMenuItem(), event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); EXPECT_FALSE(capture_label_widget->IsVisible()); EXPECT_FALSE(settings_widget->IsVisible()); // Now close folder selection window, check that capture label widget and // settings widget become visible. dialog_factory->CancelDialog(); EXPECT_FALSE(IsFolderSelectionDialogShown()); EXPECT_TRUE(capture_label_widget->IsVisible()); EXPECT_EQ(capture_label_widget->GetLayer()->GetTargetOpacity(), 1.f); EXPECT_TRUE(settings_widget->IsVisible()); // Close settings widget. Capture label widget is visible. ClickOnView(GetSettingsButton(), event_generator); EXPECT_TRUE(capture_label_widget->IsVisible()); controller->Stop(); } // Tests that when capture label widget overlaps with settings widget, capture // label widget is shown/hidden correctly after open/close the folder selection // window, open/close settings menu. Regression test for // https://crbug.com/1279606. TEST_F(CaptureModeSettingsTest, CaptureLabelViewOverlapsWithSettingsView) { // Update display size to make capture label widget overlap with settings // widget. UpdateDisplay("1100x700"); auto* controller = StartImageRegionCapture(); auto* event_generator = GetEventGenerator(); // Tests that capture label widget overlaps with settings widget and is // hidden after setting widget is shown. auto* capture_label_widget = GetCaptureModeLabelWidget(); ClickOnView(GetSettingsButton(), event_generator); auto* settings_widget = GetCaptureModeSettingsWidget(); EXPECT_TRUE(capture_label_widget->GetWindowBoundsInScreen().Intersects( settings_widget->GetWindowBoundsInScreen())); EXPECT_FALSE(GetCaptureModeLabelWidget()->IsVisible()); EXPECT_TRUE(settings_widget->IsVisible()); // Open folder selection window, capture label widget is invisible. CaptureModeSettingsTestApi test_api; auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); ClickOnView(test_api.GetSelectFolderMenuItem(), event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); EXPECT_FALSE(capture_label_widget->IsVisible()); // Close folder selection window, capture label widget is invisible. dialog_factory->CancelDialog(); EXPECT_FALSE(IsFolderSelectionDialogShown()); EXPECT_FALSE(capture_label_widget->IsVisible()); // Tests that capture label widget is visible after settings widget is // closed. ClickOnView(GetSettingsButton(), event_generator); EXPECT_TRUE(capture_label_widget->IsVisible()); EXPECT_EQ(capture_label_widget->GetLayer()->GetTargetOpacity(), 1.f); controller->Stop(); } // Tests the basic keyboard navigation functions for the settings menu. TEST_F(CaptureModeSettingsTest, KeyboardNavigationForSettingsMenu) { auto* controller = StartCaptureSession(CaptureModeSource::kRegion, CaptureModeType::kImage); using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; CaptureModeSessionTestApi session_test_api( controller->capture_mode_session()); // Tab six times to focus the settings button. auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_TAB, event_generator, /*shift_down=*/false, /*count=*/6); EXPECT_EQ(FocusGroup::kSettingsClose, session_test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, session_test_api.GetCurrentFocusIndex()); // Enter space to open the settings menu. The current focus group should be // `kPendingSettings`. SendKey(ui::VKEY_SPACE, event_generator); ASSERT_TRUE(GetCaptureModeSettingsView()); EXPECT_EQ(FocusGroup::kPendingSettings, session_test_api.GetCurrentFocusGroup()); CaptureModeSettingsTestApi settings_test_api; CaptureModeMenuGroup* audio_input_menu_group = settings_test_api.GetAudioInputMenuGroup(); // Tab once to focus the first item on the settings menu (`Audio input` // header). SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kSettingsMenu, session_test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, session_test_api.GetCurrentFocusIndex()); // Tab once to focus the `Off` option on the settings menu. Check `Off` option // is the checked option not the `Microphone`. SendKey(ui::VKEY_TAB, event_generator); EXPECT_TRUE(audio_input_menu_group->IsOptionChecked(kAudioOff)); EXPECT_FALSE(audio_input_menu_group->IsOptionChecked(kAudioMicrophone)); // Now tab once to focus the `Microphone` option and enter space to select it. // Check now `Microphone` option is the checked option not the `Off`. SendKey(ui::VKEY_TAB, event_generator); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_FALSE(audio_input_menu_group->IsOptionChecked(kAudioOff)); EXPECT_TRUE(audio_input_menu_group->IsOptionChecked(kAudioMicrophone)); // Tab three times to focus the `Select folder...` menu item and enter space // to open the selection window. SendKey(ui::VKEY_TAB, event_generator, /*shift_down=*/false, /*count=*/3); SendKey(ui::VKEY_SPACE, event_generator); auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); EXPECT_TRUE(IsFolderSelectionDialogShown()); // Close selection window. dialog_factory->CancelDialog(); EXPECT_FALSE(IsFolderSelectionDialogShown()); // Now tab once to focus on the settings button and enter space on it to close // the settings menu. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kSettingsClose, session_test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, session_test_api.GetCurrentFocusIndex()); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_FALSE(GetCaptureModeSettingsView()); } // Tests that the disabled option in the settings menu will be skipped while // tabbing through. TEST_F(CaptureModeSettingsTest, KeyboardNavigationForSettingsMenuWithDisabledOption) { // Begin a new session with a pre-configured unavailable custom folder. auto* controller = CaptureModeController::Get(); const base::FilePath custom_folder(FILE_PATH_LITERAL("/home/random")); controller->SetCustomCaptureFolder(custom_folder); StartImageRegionCapture(); using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; CaptureModeSessionTestApi session_test_api( controller->capture_mode_session()); // Tab six times to focus the settings button and enter space to open the // setting menu. auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_TAB, event_generator, /*shift_down=*/false, /*count=*/6); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(FocusGroup::kPendingSettings, session_test_api.GetCurrentFocusGroup()); CaptureModeSettingsView* settings_menu = GetCaptureModeSettingsView(); ASSERT_TRUE(settings_menu); // Since the custom folder is unavailable, the `kCustomFolder` should be // disabled and won't be returned via // `CaptureModeSettingsViews::GetHighlightableItems`. CaptureModeSettingsTestApi settings_test_api; auto* custom_folder_view = settings_test_api.GetCustomFolderOptionIfAny(); ASSERT_TRUE(custom_folder_view); EXPECT_FALSE(custom_folder_view->GetEnabled()); std::vector highlightable_items = settings_menu->GetHighlightableItems(); EXPECT_TRUE( std::find_if(highlightable_items.begin(), highlightable_items.end(), [custom_folder_view]( CaptureModeSessionFocusCycler::HighlightableView* item) { return item->GetView() == custom_folder_view; }) == highlightable_items.end()); // Tab five times to focus the default `Downloads` option. SendKey(ui::VKEY_TAB, event_generator, /*shift_down=*/false, /*count=*/5); EXPECT_EQ(session_test_api.GetCurrentFocusedView()->GetView(), settings_test_api.GetDefaultDownloadsOption()); // Tab once to check the disabled `kCustomFolder` option is skipped and now // the `Select folder...` menu item gets focused. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(session_test_api.GetCurrentFocusedView()->GetView(), settings_test_api.GetSelectFolderMenuItem()); } // Tests that selecting the default `Downloads` folder as the custom folder via // keyboard navigation doesn't lead to a crash. Regression test for // https://crbug.com/1269373. TEST_F(CaptureModeSettingsTest, KeyboardNavigationForRemovingCustomFolderOption) { // Begin a new session with a pre-configured custom folder. auto* controller = CaptureModeController::Get(); const base::FilePath custom_folder( CreateCustomFolderInUserDownloadsPath("test")); controller->SetCustomCaptureFolder(custom_folder); StartImageRegionCapture(); using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; CaptureModeSessionTestApi test_api(controller->capture_mode_session()); // Tab six times to focus the settings button, then enter space to open the // setting menu. Wait for the setting menu to be refreshed. auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_TAB, event_generator, /*shift_down=*/false, /*count=*/6); SendKey(ui::VKEY_SPACE, event_generator); WaitForSettingsMenuToBeRefreshed(); EXPECT_EQ(FocusGroup::kPendingSettings, test_api.GetCurrentFocusGroup()); CaptureModeSettingsView* settings_menu = GetCaptureModeSettingsView(); ASSERT_TRUE(settings_menu); // Tab seven times to focus the `Select folder...` menu item and enter space // to open the selection window. SendKey(ui::VKEY_TAB, event_generator, /*shift_down=*/false, /*count=*/7); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); // The current focus group is `FocusGroup::kSettingsMenu` and focus index is // 6u. EXPECT_EQ(FocusGroup::kSettingsMenu, test_api.GetCurrentFocusGroup()); EXPECT_EQ(6u, test_api.GetCurrentFocusIndex()); // Select the default `Downloads` folder as the custom folder which will // have custom folder option get removed. auto* test_delegate = controller->delegate_for_testing(); const auto default_downloads_folder = test_delegate->GetUserDefaultDownloadsFolder(); auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); dialog_factory->AcceptPath(default_downloads_folder); // Press space to ensure the selection window can be opened after the custom // folder is removed from the settings menu. SendKey(ui::VKEY_SPACE, event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); dialog_factory->CancelDialog(); // Tab once to make sure there's no crash and the focus gets moved to // settings button. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kSettingsClose, test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, test_api.GetCurrentFocusIndex()); } // Tests that first time selecting a custom folder via keyboard navigation. // After the custom folder is selected, tabbing one more time will move focus // from the settings menu to the settings button. TEST_F(CaptureModeSettingsTest, KeyboardNavigationForAddingCustomFolderOption) { auto* controller = CaptureModeController::Get(); StartImageRegionCapture(); using FocusGroup = CaptureModeSessionFocusCycler::FocusGroup; CaptureModeSessionTestApi session_test_api( controller->capture_mode_session()); // Tab six times to focus the settings button, then enter space to open the // setting menu. auto* event_generator = GetEventGenerator(); SendKey(ui::VKEY_TAB, event_generator, /*shift_down=*/false, /*count=*/6); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_EQ(FocusGroup::kPendingSettings, session_test_api.GetCurrentFocusGroup()); CaptureModeSettingsView* settings_menu = GetCaptureModeSettingsView(); ASSERT_TRUE(settings_menu); // Tab six times to focus the `Select folder...` menu item and enter space // to open the selection window. SendKey(ui::VKEY_TAB, event_generator, /*shift_down=*/false, /*count=*/6); SendKey(ui::VKEY_SPACE, event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); // The current focus group is `FocusGroup::kSettingsMenu` and focus index is // 5u. EXPECT_EQ(FocusGroup::kSettingsMenu, session_test_api.GetCurrentFocusGroup()); EXPECT_EQ(5u, session_test_api.GetCurrentFocusIndex()); // Select the custom folder. Wait for the settings menu to be refreshed. The // custom folder option should be added to the settings menu and checked. const base::FilePath custom_folder( CreateCustomFolderInUserDownloadsPath("test")); controller->SetCustomCaptureFolder(custom_folder); auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); dialog_factory->AcceptPath(custom_folder); WaitForSettingsMenuToBeRefreshed(); CaptureModeSettingsTestApi settings_test_api; EXPECT_TRUE(settings_test_api.GetCustomFolderOptionIfAny()); // Press space to ensure the selection window can be opened after the custom // folder is added to the settings menu. SendKey(ui::VKEY_SPACE, event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); dialog_factory->CancelDialog(); // Tab once to make sure the focus gets moved to settings button. SendKey(ui::VKEY_TAB, event_generator); EXPECT_EQ(FocusGroup::kSettingsClose, session_test_api.GetCurrentFocusGroup()); EXPECT_EQ(0u, session_test_api.GetCurrentFocusIndex()); } // ----------------------------------------------------------------------------- // CaptureModeHistogramTest: // Test fixture to verify screen capture histograms depending on the test // param (true for tablet mode, false for clamshell mode). class CaptureModeHistogramTest : public CaptureModeSettingsTest, public ::testing::WithParamInterface { public: CaptureModeHistogramTest() = default; ~CaptureModeHistogramTest() override = default; // CaptureModeSettingsTest: void SetUp() override { CaptureModeSettingsTest::SetUp(); if (GetParam()) SwitchToTabletMode(); } void StartSessionForVideo() { StartCaptureSession(CaptureModeSource::kFullscreen, CaptureModeType::kVideo); } void StartRecording() { CaptureModeTestApi().PerformCapture(); } void StopRecording() { CaptureModeTestApi().StopVideoRecording(); } void OpenView(const views::View* view, ui::test::EventGenerator* event_generator) { if (GetParam()) TouchOnView(view, event_generator); else ClickOnView(view, event_generator); } }; TEST_P(CaptureModeHistogramTest, VideoRecordingAudioMetric) { constexpr char kHistogramNameBase[] = "Ash.CaptureModeController.CaptureAudioOnMetric"; base::HistogramTester histogram_tester; histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), false, 0); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), true, 0); // Perform a video recording with audio off. A false should be recorded. StartSessionForVideo(); CaptureModeTestApi().SetAudioRecordingEnabled(false); StartRecording(); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), false, 1); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), true, 0); StopRecording(); WaitForCaptureFileToBeSaved(); // Perform a video recording with audio on. A true should be recorded. StartSessionForVideo(); CaptureModeTestApi().SetAudioRecordingEnabled(true); StartRecording(); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), false, 1); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), true, 1); StopRecording(); } TEST_P(CaptureModeHistogramTest, CaptureModeSwitchToDefaultReasonMetric) { constexpr char kHistogramNameBase[] = "Ash.CaptureModeController.SwitchToDefaultReason"; base::HistogramTester histogram_tester; auto* controller = CaptureModeController::Get(); const auto downloads_folder = controller->delegate_for_testing()->GetUserDefaultDownloadsFolder(); const base::FilePath non_available_custom_folder( FILE_PATH_LITERAL("/home/test")); const base::FilePath available_custom_folder = CreateCustomFolderInUserDownloadsPath("test"); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), CaptureModeSwitchToDefaultReason::kFolderUnavailable, 0); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), CaptureModeSwitchToDefaultReason::kUserSelectedFromFolderSelectionDialog, 0); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), CaptureModeSwitchToDefaultReason::kUserSelectedFromSettingsMenu, 0); StartImageRegionCapture(); // Set the custom folder to an unavailable folder the switch to default // reason should be recorded as kFolderUnavailable. controller->SetCustomCaptureFolder(non_available_custom_folder); EXPECT_EQ(controller->GetCurrentCaptureFolder().path, non_available_custom_folder); auto* event_generator = GetEventGenerator(); OpenView(GetSettingsButton(), event_generator); WaitForSettingsMenuToBeRefreshed(); CaptureModeSettingsTestApi test_api; CaptureModeMenuGroup* save_to_menu_group = test_api.GetSaveToMenuGroup(); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), CaptureModeSwitchToDefaultReason::kFolderUnavailable, 1); // Select the save-to location to default downloads folder from folder // selection dialog and the switch to default reason should be recorded as // kUserSelectedFromSettingsMenu. controller->SetCustomCaptureFolder(available_custom_folder); EXPECT_EQ(controller->GetCurrentCaptureFolder().path, available_custom_folder); OpenView(test_api.GetSelectFolderMenuItem(), event_generator); EXPECT_TRUE(IsFolderSelectionDialogShown()); auto* dialog_factory = FakeFolderSelectionDialogFactory::Get(); dialog_factory->AcceptPath(downloads_folder); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), CaptureModeSwitchToDefaultReason::kUserSelectedFromFolderSelectionDialog, 1); // Select the save-to location to default downloads folder from settings // menu and the switch to default reason should be recorded as // kUserSelectedFromFolderSelectionDialog. controller->SetCustomCaptureFolder(available_custom_folder); EXPECT_EQ(controller->GetCurrentCaptureFolder().path, available_custom_folder); OpenView(test_api.GetDefaultDownloadsOption(), event_generator); EXPECT_TRUE(save_to_menu_group->IsOptionChecked(kDownloadsFolder)); histogram_tester.ExpectBucketCount( GetCaptureModeHistogramName(kHistogramNameBase), CaptureModeSwitchToDefaultReason::kUserSelectedFromSettingsMenu, 1); } INSTANTIATE_TEST_SUITE_P(All, CaptureModeHistogramTest, ::testing::Bool()); } // namespace ash