blob: d118b959aed5e3c9ff36fbd6adf55eff0d12b516 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/overlay/video_overlay_window_views.h"
#include <memory>
#include <utility>
#include "base/containers/contains.h"
#include "base/memory/raw_ptr.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/picture_in_picture/auto_pip_setting_overlay_view.h"
#include "chrome/browser/picture_in_picture/picture_in_picture_occlusion_tracker.h"
#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
#include "chrome/browser/ui/views/overlay/close_image_button.h"
#include "chrome/browser/ui/views/overlay/simple_overlay_window_image_button.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "content/public/browser/overlay_window.h"
#include "content/public/browser/video_picture_in_picture_window_controller.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/test_web_contents_factory.h"
#include "media/base/media_switches.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/compositor/layer.h"
#include "ui/display/test/test_screen.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/test/button_test_api.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/widget/widget_utils.h"
namespace {
constexpr gfx::Size kMinWindowSize(200, 100);
// Reported minimum bubble size for the setting view. The pip window should be
// larger than this when the setting view is shown.
constexpr gfx::Size kBubbleSize(300, 200);
// Size that's big enough to accommodate a `kBubbleSize`-sized bubble without
// being further adjusted upwards for margin.
constexpr gfx::Size kSizeBigEnoughForBubble(400, 300);
} // namespace
using testing::_;
using ::testing::Return;
// Mock of AutoPipSettingOverlayView. Used for injection during tests.
class MockOverlayView : public AutoPipSettingOverlayView {
public:
explicit MockOverlayView(views::View* anchor_view)
: AutoPipSettingOverlayView(base::DoNothing(),
GURL{"https://example.com"},
gfx::Rect(),
anchor_view,
views::BubbleBorder::Arrow::FLOAT) {}
MOCK_METHOD(void, ShowBubble, (gfx::NativeView parent), (override));
void SetWantsEvent(bool wants_event) { wants_event_ = wants_event; }
bool WantsEvent(const gfx::Point& point_in_screen) override {
// Consume any event we're given. The goal is to make sure we're given the
// opportunity to take an event.
return wants_event_;
}
gfx::Size GetBubbleSize() const override {
// Return something that's bigger than the minimum.
return kBubbleSize;
}
private:
bool wants_event_ = false;
};
class TestVideoPictureInPictureWindowController
: public content::VideoPictureInPictureWindowController {
public:
TestVideoPictureInPictureWindowController() = default;
// PictureInPictureWindowController:
void Show() override {}
void FocusInitiator() override {}
MOCK_METHOD(void, Close, (bool));
void CloseAndFocusInitiator() override {}
MOCK_METHOD(void, OnWindowDestroyed, (bool));
content::VideoOverlayWindow* GetWindowForTesting() override {
return nullptr;
}
void UpdateLayerBounds() override {}
bool IsPlayerActive() override { return false; }
void set_web_contents(content::WebContents* web_contents) {
web_contents_ = web_contents;
}
content::WebContents* GetWebContents() override { return web_contents_; }
content::WebContents* GetChildWebContents() override { return nullptr; }
bool TogglePlayPause() override { return false; }
void SkipAd() override {}
void NextTrack() override {}
void PreviousTrack() override {}
void NextSlide() override {}
void PreviousSlide() override {}
void ToggleMicrophone() override {}
void ToggleCamera() override {}
void HangUp() override {}
const gfx::Rect& GetSourceBounds() const override { return source_bounds_; }
std::optional<gfx::Rect> GetWindowBounds() override { return std::nullopt; }
std::optional<url::Origin> GetOrigin() override { return std::nullopt; }
void SetOnWindowCreatedNotifyObserversCallback(base::OnceClosure) override {}
private:
raw_ptr<content::WebContents> web_contents_;
gfx::Rect source_bounds_;
};
class VideoOverlayWindowViewsTest : public ChromeViewsTestBase {
public:
VideoOverlayWindowViewsTest() = default;
// ChromeViewsTestBase:
void SetUp() override {
feature_list_.InitAndEnableFeature(
media::kPictureInPictureOcclusionTracking);
display::Screen::SetScreenInstance(&test_screen_);
// Purposely skip ChromeViewsTestBase::SetUp() as that creates ash::Shell
// on ChromeOS, which we don't want.
ViewsTestBase::SetUp();
// web_contents_ needs to be created after the constructor, so that
// |feature_list_| can be initialized before other threads check if a
// feature is enabled.
web_contents_ = web_contents_factory_.CreateWebContents(&profile_);
pip_window_controller_.set_web_contents(web_contents_);
#if BUILDFLAG(IS_CHROMEOS)
test_views_delegate()->set_context(GetContext());
#endif
test_views_delegate()->set_use_desktop_native_widgets(true);
// The default work area must be big enough to fit the minimum
// VideoOverlayWindowViews size.
SetDisplayWorkArea({0, 0, 1000, 1000});
overlay_window_ = VideoOverlayWindowViews::Create(&pip_window_controller_);
overlay_window_->set_overlay_view_cb_for_testing(
base::BindRepeating(&VideoOverlayWindowViewsTest::GetOverlayViewImpl,
base::Unretained(this)));
// On some platforms, OnNativeWidgetMove is invoked on creation.
WaitForMove();
overlay_window_->set_minimum_size_for_testing(kMinWindowSize);
event_generator_ = std::make_unique<ui::test::EventGenerator>(
views::GetRootWindow(overlay_window_.get()));
}
void TearDown() override {
overlay_window_.reset();
ViewsTestBase::TearDown();
display::Screen::SetScreenInstance(nullptr);
}
void SetDisplayWorkArea(const gfx::Rect& work_area) {
display::Display display = test_screen_.GetPrimaryDisplay();
display.set_work_area(work_area);
test_screen_.display_list().UpdateDisplay(display);
}
VideoOverlayWindowViews& overlay_window() { return *overlay_window_; }
content::WebContents* web_contents() { return web_contents_; }
TestVideoPictureInPictureWindowController& pip_window_controller() {
return pip_window_controller_;
}
MockOverlayView* SetOverlayView() {
std::unique_ptr<MockOverlayView> mock_overlay_view =
std::make_unique<MockOverlayView>(
overlay_window().window_background_view_for_testing());
overlay_view_ = std::move(mock_overlay_view);
return overlay_view_.get();
}
ui::test::EventGenerator* event_generator() { return event_generator_.get(); }
protected:
void WaitForMove() {
task_environment()->FastForwardBy(
VideoOverlayWindowViews::kControlHideDelayAfterMove +
base::Milliseconds(1));
}
void DestroyOverlayWindow() { overlay_window_.reset(); }
private:
std::unique_ptr<AutoPipSettingOverlayView> GetOverlayViewImpl() {
return std::move(overlay_view_);
}
TestingProfile profile_;
content::TestWebContentsFactory web_contents_factory_;
raw_ptr<content::WebContents> web_contents_;
TestVideoPictureInPictureWindowController pip_window_controller_;
display::test::TestScreen test_screen_;
// Overlay view that we'll send to the window. May be null.
std::unique_ptr<MockOverlayView> overlay_view_;
std::unique_ptr<ui::test::EventGenerator> event_generator_;
std::unique_ptr<VideoOverlayWindowViews> overlay_window_;
base::test::ScopedFeatureList feature_list_;
};
TEST_F(VideoOverlayWindowViewsTest, InitialWindowSize_Square) {
// Fit the window taking 1/5 (both dimensions) of the work area as the
// starting size, and applying the size and aspect ratio constraints.
overlay_window().UpdateNaturalSize({400, 400});
EXPECT_EQ(gfx::Size(200, 200), overlay_window().GetBounds().size());
EXPECT_EQ(gfx::Size(200, 200),
overlay_window().video_layer_for_testing()->size());
}
TEST_F(VideoOverlayWindowViewsTest, InitialWindowSize_Horizontal) {
// Fit the window taking 1/5 (both dimensions) of the work area as the
// starting size, and applying the size and aspect ratio constraints.
overlay_window().UpdateNaturalSize({400, 200});
EXPECT_EQ(gfx::Size(400, 200), overlay_window().GetBounds().size());
EXPECT_EQ(gfx::Size(400, 200),
overlay_window().video_layer_for_testing()->size());
}
TEST_F(VideoOverlayWindowViewsTest, InitialWindowSize_Vertical) {
// Fit the window taking 1/5 (both dimensions) of the work area as the
// starting size, and applying the size and aspect ratio constraints.
overlay_window().UpdateNaturalSize({400, 500});
EXPECT_EQ(gfx::Size(200, 250), overlay_window().GetBounds().size());
EXPECT_EQ(gfx::Size(200, 250),
overlay_window().video_layer_for_testing()->size());
}
TEST_F(VideoOverlayWindowViewsTest, Letterboxing) {
overlay_window().UpdateNaturalSize({400, 10});
// Must fit within the minimum height of 146. But with the aspect ratio of
// 40:1 the width gets exceedingly big and must be limited to the maximum of
// 800. Thus, letterboxing is unavoidable.
EXPECT_EQ(gfx::Size(800, 100), overlay_window().GetBounds().size());
EXPECT_EQ(gfx::Size(800, 20),
overlay_window().video_layer_for_testing()->size());
}
TEST_F(VideoOverlayWindowViewsTest, Pillarboxing) {
overlay_window().UpdateNaturalSize({10, 400});
// Must fit within the minimum width of 260. But with the aspect ratio of
// 1:40 the height gets exceedingly big and must be limited to the maximum of
// 800. Thus, pillarboxing is unavoidable.
EXPECT_EQ(gfx::Size(200, 800), overlay_window().GetBounds().size());
EXPECT_EQ(gfx::Size(20, 800),
overlay_window().video_layer_for_testing()->size());
}
TEST_F(VideoOverlayWindowViewsTest, Pillarboxing_Square) {
overlay_window().UpdateNaturalSize({100, 100});
// Pillarboxing also occurs on Linux even with the square aspect ratio,
// because the user is allowed to size the window to the rectangular minimum
// size.
overlay_window().SetSize({200, 100});
EXPECT_EQ(gfx::Size(100, 100),
overlay_window().video_layer_for_testing()->size());
}
TEST_F(VideoOverlayWindowViewsTest, ApproximateAspectRatio_Horizontal) {
// "Horizontal" video.
overlay_window().UpdateNaturalSize({320, 240});
// The user drags the window resizer horizontally and now the integer window
// dimensions can't reproduce the video aspect ratio exactly. The video
// should still fill the entire window area.
overlay_window().SetSize({320, 240});
EXPECT_EQ(gfx::Size(320, 240),
overlay_window().video_layer_for_testing()->size());
overlay_window().SetSize({321, 241});
EXPECT_EQ(gfx::Size(321, 241),
overlay_window().video_layer_for_testing()->size());
// Wide video.
overlay_window().UpdateNaturalSize({1600, 900});
overlay_window().SetSize({444, 250});
EXPECT_EQ(gfx::Size(444, 250),
overlay_window().video_layer_for_testing()->size());
overlay_window().SetSize({445, 250});
EXPECT_EQ(gfx::Size(445, 250),
overlay_window().video_layer_for_testing()->size());
// Very wide video.
overlay_window().UpdateNaturalSize({400, 100});
overlay_window().SetSize({478, 120});
EXPECT_EQ(gfx::Size(478, 120),
overlay_window().video_layer_for_testing()->size());
overlay_window().SetSize({481, 120});
EXPECT_EQ(gfx::Size(481, 120),
overlay_window().video_layer_for_testing()->size());
}
TEST_F(VideoOverlayWindowViewsTest, ApproximateAspectRatio_Vertical) {
// "Vertical" video.
overlay_window().UpdateNaturalSize({240, 320});
// The user dragged the window resizer vertically and now the integer window
// dimensions can't reproduce the video aspect ratio exactly. The video
// should still fill the entire window area.
overlay_window().SetSize({240, 320});
EXPECT_EQ(gfx::Size(240, 320),
overlay_window().video_layer_for_testing()->size());
overlay_window().SetSize({239, 319});
EXPECT_EQ(gfx::Size(239, 319),
overlay_window().video_layer_for_testing()->size());
// Narrow video.
overlay_window().UpdateNaturalSize({900, 1600});
overlay_window().SetSize({250, 444});
EXPECT_EQ(gfx::Size(250, 444),
overlay_window().video_layer_for_testing()->size());
overlay_window().SetSize({250, 445});
EXPECT_EQ(gfx::Size(250, 445),
overlay_window().video_layer_for_testing()->size());
// Very narrow video.
// NOTE: Window width is bounded by the minimum size.
overlay_window().UpdateNaturalSize({100, 400});
overlay_window().SetSize({200, 478});
EXPECT_EQ(gfx::Size(120, 478),
overlay_window().video_layer_for_testing()->size());
overlay_window().SetSize({200, 481});
EXPECT_EQ(gfx::Size(120, 481),
overlay_window().video_layer_for_testing()->size());
}
TEST_F(VideoOverlayWindowViewsTest, UpdateMaximumSize) {
SetDisplayWorkArea({0, 0, 4000, 4000});
overlay_window().UpdateNaturalSize({480, 320});
// The initial size is determined by the work area and the video natural size
// (aspect ratio).
EXPECT_EQ(gfx::Size(1200, 800), overlay_window().GetBounds().size());
// The initial maximum size is 80% of the work area.
EXPECT_EQ(gfx::Size(3200, 3200), overlay_window().GetMaximumSize());
// If the maximum size increases then we should keep the existing window size.
SetDisplayWorkArea({0, 0, 8000, 8000});
EXPECT_EQ(gfx::Size(1200, 800), overlay_window().GetBounds().size());
EXPECT_EQ(gfx::Size(6400, 6400), overlay_window().GetMaximumSize());
// If the maximum size decreases then we should shrink to fit.
SetDisplayWorkArea({0, 0, 1000, 2000});
EXPECT_EQ(gfx::Size(800, 800), overlay_window().GetBounds().size());
EXPECT_EQ(gfx::Size(800, 1600), overlay_window().GetMaximumSize());
}
TEST_F(VideoOverlayWindowViewsTest, IgnoreInvalidMaximumSize) {
ASSERT_EQ(gfx::Size(800, 800), overlay_window().GetMaximumSize());
SetDisplayWorkArea({0, 0, 0, 0});
EXPECT_EQ(gfx::Size(800, 800), overlay_window().GetMaximumSize());
}
// Tests that Next Track button bounds are updated right away when window
// controls are hidden.
TEST_F(VideoOverlayWindowViewsTest, NextTrackButtonAddedWhenControlsHidden) {
ASSERT_FALSE(overlay_window().AreControlsVisible());
ASSERT_TRUE(overlay_window()
.next_track_controls_view_for_testing()
->size()
.IsEmpty());
const auto origin_before_layout =
overlay_window().next_track_controls_view_for_testing()->origin();
overlay_window().SetNextTrackButtonVisibility(true);
EXPECT_NE(overlay_window().next_track_controls_view_for_testing()->origin(),
origin_before_layout);
EXPECT_FALSE(overlay_window().IsLayoutPendingForTesting());
}
// Tests that Previous Track button bounds are updated right away when window
// controls are hidden.
TEST_F(VideoOverlayWindowViewsTest,
PreviousTrackButtonAddedWhenControlsHidden) {
ASSERT_FALSE(overlay_window().AreControlsVisible());
ASSERT_TRUE(overlay_window()
.previous_track_controls_view_for_testing()
->size()
.IsEmpty());
const auto origin_before_layout =
overlay_window().previous_track_controls_view_for_testing()->origin();
overlay_window().SetPreviousTrackButtonVisibility(true);
EXPECT_NE(
overlay_window().previous_track_controls_view_for_testing()->origin(),
origin_before_layout);
EXPECT_FALSE(overlay_window().IsLayoutPendingForTesting());
}
TEST_F(VideoOverlayWindowViewsTest, UpdateNaturalSizeDoesNotMoveWindow) {
// Enter PiP.
overlay_window().UpdateNaturalSize({300, 200});
overlay_window().ShowInactive();
// Resize the window and move it toward the top-left corner of the work area.
// In production, resizing preserves the aspect ratio if possible, so we
// preserve it here too.
overlay_window().SetBounds({100, 100, 450, 300});
// Simulate a new surface layer and a change in the aspect ratio.
overlay_window().UpdateNaturalSize({400, 200});
// The window should not move.
// The window size will be adjusted according to the new aspect ratio, and
// clamped to 600x300 to fit within the maximum size for the work area of
// 1000x1000.
EXPECT_EQ(gfx::Rect(100, 100, 600, 300), overlay_window().GetBounds());
}
// Tests that the OverlayWindowFrameView does not accept events so they can
// propagate to the overlay.
TEST_F(VideoOverlayWindowViewsTest, HitTestFrameView) {
// Since the NonClientFrameView is the only non-custom direct descendent of
// the NonClientView, we can assume that if the frame does not accept the
// point but the NonClientView does, then it will be handled by one of the
// custom overlay views.
auto point = gfx::Point(50, 50);
views::NonClientView* non_client_view = overlay_window().non_client_view();
EXPECT_EQ(non_client_view->frame_view()->HitTestPoint(point), false);
EXPECT_EQ(non_client_view->HitTestPoint(point), true);
}
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
// With pillarboxing, the close button doesn't cover the video area. Make sure
// hovering the button doesn't get handled like normal mouse exit events
// causing the controls to hide.
// TODO(http://crbug/1509791): Fix and re-enable.
TEST_F(VideoOverlayWindowViewsTest, DISABLED_NoMouseExitWithinWindowBounds) {
overlay_window().UpdateNaturalSize({10, 400});
WaitForMove();
const auto close_button_bounds = overlay_window().GetCloseControlsBounds();
const auto video_bounds =
overlay_window().video_layer_for_testing()->bounds();
ASSERT_FALSE(video_bounds.Contains(close_button_bounds));
const gfx::Point moved_location(video_bounds.origin() + gfx::Vector2d(5, 5));
ui::MouseEvent moved_event(ui::ET_MOUSE_MOVED, moved_location, moved_location,
ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE);
overlay_window().OnMouseEvent(&moved_event);
ASSERT_TRUE(overlay_window().AreControlsVisible());
const gfx::Point exited_location(close_button_bounds.CenterPoint());
ui::MouseEvent exited_event(ui::ET_MOUSE_EXITED, exited_location,
exited_location, ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
overlay_window().OnMouseEvent(&exited_event);
EXPECT_TRUE(overlay_window().AreControlsVisible());
}
#endif // !BUILDFLAG(IS_CHROMEOS_LACROS)
TEST_F(VideoOverlayWindowViewsTest, ShowControlsOnFocus) {
EXPECT_FALSE(overlay_window().AreControlsVisible());
overlay_window().OnNativeFocus();
EXPECT_TRUE(overlay_window().AreControlsVisible());
}
TEST_F(VideoOverlayWindowViewsTest, OnlyPauseOnCloseWhenPauseIsAvailable) {
views::test::ButtonTestApi close_button_clicker(
overlay_window().close_button_for_testing());
ui::MouseEvent dummy_event(ui::ET_MOUSE_PRESSED, gfx::Point(0, 0),
gfx::Point(0, 0), ui::EventTimeForNow(), 0, 0);
// When the play/pause controls are visible, closing via the close button
// should pause the video.
overlay_window().SetPlayPauseButtonVisibility(true);
PictureInPictureWindowManager::GetInstance()
->set_window_controller_for_testing(&pip_window_controller());
EXPECT_CALL(pip_window_controller(), Close(true));
close_button_clicker.NotifyClick(dummy_event);
testing::Mock::VerifyAndClearExpectations(&pip_window_controller());
// When the play/pause controls are not visible, closing via the close button
// should not pause the video.
overlay_window().SetPlayPauseButtonVisibility(false);
EXPECT_CALL(pip_window_controller(), Close(false));
close_button_clicker.NotifyClick(dummy_event);
testing::Mock::VerifyAndClearExpectations(&pip_window_controller());
PictureInPictureWindowManager::GetInstance()
->set_window_controller_for_testing(nullptr);
}
TEST_F(VideoOverlayWindowViewsTest, PauseOnWidgetCloseWhenPauseAvailable) {
// When the play/pause controls are visible, when the native widget is
// destroyed we should pause the underlying video.
overlay_window().SetPlayPauseButtonVisibility(true);
EXPECT_CALL(pip_window_controller(), OnWindowDestroyed(true));
overlay_window().CloseNow();
testing::Mock::VerifyAndClearExpectations(&pip_window_controller());
}
TEST_F(VideoOverlayWindowViewsTest,
DontPauseOnWidgetCloseWhenPauseNotAvailable) {
// When the play/pause controls are not visible, when the native widget is
// destroyed we should not pause the underlying video.
overlay_window().SetPlayPauseButtonVisibility(false);
EXPECT_CALL(pip_window_controller(), OnWindowDestroyed(false));
overlay_window().CloseNow();
testing::Mock::VerifyAndClearExpectations(&pip_window_controller());
}
TEST_F(VideoOverlayWindowViewsTest, SmallDisplayWorkAreaDoesNotCrash) {
SetDisplayWorkArea({0, 0, 240, 120});
overlay_window().UpdateNaturalSize({400, 300});
// Since the work area would force a max size smaller than the minimum size,
// the size is fixed at the minimum size.
EXPECT_EQ(kMinWindowSize, overlay_window().GetBounds().size());
EXPECT_EQ(kMinWindowSize, overlay_window().GetMaximumSize());
// The video should still be letterboxed to the correct aspect ratio.
EXPECT_EQ(gfx::Size(133, 100),
overlay_window().video_layer_for_testing()->size());
}
// TODO(http://crbug/1509791): Fix and re-enable.
TEST_F(VideoOverlayWindowViewsTest, DISABLED_ControlsAreHiddenDuringMove) {
// Set the initial position.
overlay_window().SetBounds({0, 0, 100, 100});
WaitForMove();
// Make the controls visible.
overlay_window().UpdateControlsVisibility(true);
ASSERT_TRUE(overlay_window().AreControlsVisible());
// Now move the window, this should cause the controls to be hidden.
overlay_window().SetBounds({50, 0, 100, 100});
EXPECT_FALSE(overlay_window().AreControlsVisible());
// Should still be hidden with mouse event.
overlay_window().UpdateControlsVisibility(true);
EXPECT_FALSE(overlay_window().AreControlsVisible());
// After moving, overlay should be visible again because of the previous
// mouse event.
WaitForMove();
EXPECT_TRUE(overlay_window().AreControlsVisible());
}
TEST_F(VideoOverlayWindowViewsTest,
ControlsAreHiddenDuringMove_MultipleUpdates) {
overlay_window().SetBounds({0, 0, 100, 100});
WaitForMove();
// Move the window.
overlay_window().SetBounds({50, 0, 100, 100});
EXPECT_FALSE(overlay_window().AreControlsVisible());
overlay_window().UpdateControlsVisibility(true);
overlay_window().UpdateControlsVisibility(false);
overlay_window().UpdateControlsVisibility(true);
overlay_window().UpdateControlsVisibility(false);
// Only the last one should have any effect.
EXPECT_FALSE(overlay_window().AreControlsVisible());
}
TEST_F(VideoOverlayWindowViewsTest, OverlayViewIsSizedCorrectly) {
// Set the bound of the window before showing it, to make sure the size
// propagates to the overlay view. We use the larger-than-bubble size so that
// it should be an exact match. If it were too small, then the overlay window
// might have to be even larger than we request to fit the bubble.
// Setting the overlay view before show should be sufficient for it to take
// effect when shown.
auto* overlay_view = SetOverlayView();
overlay_window().ShowInactive();
// Do this after showing it, else the window will size to a default size,
// rather than the bounds we request.
const gfx::Rect bounds(gfx::Point(0, 0), kSizeBigEnoughForBubble);
overlay_window().UpdateNaturalSize(bounds.size());
overlay_window().SetBounds(bounds);
EXPECT_TRUE(overlay_view->GetVisible());
EXPECT_EQ(overlay_view->bounds(), bounds);
}
TEST_F(VideoOverlayWindowViewsTest, OverlayViewCanBeClicked) {
// Make sure that the overlay view is z-ordered to get input events.
auto* overlay_view = SetOverlayView();
overlay_view->SetWantsEvent(true);
// Add a button!
base::MockRepeatingCallback<void(const ui::Event&)> cb;
auto* button = overlay_view->AddChildView(
std::make_unique<views::LabelButton>(cb.Get()));
button->SetBounds(0, 0, 50, 50);
// Show the window and click the button.
overlay_window().ShowInactive();
EXPECT_CALL(cb, Run(_));
event_generator()->MoveMouseTo(button->GetBoundsInScreen().CenterPoint());
event_generator()->ClickLeftButton();
// Clear the callback since `cb` is going away. Note that `DoNothing()`
// doesn't work here because type inference fails.
button->SetCallback(base::BindRepeating([](const ui::Event&) {}));
}
TEST_F(VideoOverlayWindowViewsTest, OverlayWindowBlocksInput) {
// Make sure that the playback controls don't receive input events while the
// overlay view is visible.
auto* overlay_view = SetOverlayView();
overlay_view->SetWantsEvent(true);
overlay_window().ShowInactive();
// When the play/pause controls are visible, closing via the close button
// should pause the video.
overlay_window().SetPlayPauseButtonVisibility(true);
EXPECT_CALL(pip_window_controller(), Close(true)).Times(0);
event_generator()->MoveMouseTo(
overlay_window().GetCloseControlsBounds().CenterPoint());
event_generator()->ClickLeftButton();
}
TEST_F(VideoOverlayWindowViewsTest, OverlayWindowFitsInMinimumSize) {
auto* overlay_view = SetOverlayView();
overlay_window().ShowInactive();
// The window size should be strictly greater than the bubble size so that
// there's some nonzero margin.
auto window_min_size = overlay_window().GetMinimumSize();
auto bubble_min_size = overlay_view->GetBubbleSize();
EXPECT_GT(window_min_size.width(), bubble_min_size.width());
EXPECT_GT(window_min_size.height(), bubble_min_size.height());
// When the overlay view is hidden, the minimum size should return to normal.
overlay_view->SetVisible(false);
EXPECT_EQ(overlay_window().GetMinimumSize(), kMinWindowSize);
}
TEST_F(VideoOverlayWindowViewsTest, OverlayWindowStopsBlockingInput) {
auto* overlay_view = SetOverlayView();
overlay_window().ShowInactive();
// Make sure that the overlay window blocks input, when the overlay view does
// not want events.
const auto close_controls_center_point =
overlay_window().GetCloseControlsBounds().CenterPoint();
overlay_view->SetWantsEvent(false);
EXPECT_FALSE(overlay_window().ControlsHitTestContainsPoint(
close_controls_center_point));
// Make sure that the overlay window stops blocking input, when the overlay
// view wants event.
overlay_view->SetWantsEvent(true);
EXPECT_TRUE(overlay_window().ControlsHitTestContainsPoint(
close_controls_center_point));
}
TEST_F(VideoOverlayWindowViewsTest, IsTrackedByTheOcclusionObserver) {
overlay_window().ShowInactive();
PictureInPictureOcclusionTracker* tracker =
PictureInPictureWindowManager::GetInstance()->GetOcclusionTracker();
// Check that the PictureInPictureOcclusionTracker is observing the
// VideoOverlayWindowViews.
EXPECT_TRUE(base::Contains(tracker->GetPictureInPictureWidgetsForTesting(),
&overlay_window()));
// Check that it's no longer observed when the widget is destroyed.
DestroyOverlayWindow();
EXPECT_EQ(0u, tracker->GetPictureInPictureWidgetsForTesting().size());
}