chromium/chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/public/browser/picture_in_picture_window_controller.h"

#include "base/barrier_closure.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/scoped_observation.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/chrome_content_browser_client.h"
#include "chrome/browser/devtools/devtools_window_testing.h"
#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/overlay/hang_up_button.h"
#include "chrome/browser/ui/views/overlay/playback_image_button.h"
#include "chrome/browser/ui/views/overlay/simple_overlay_window_image_button.h"
#include "chrome/browser/ui/views/overlay/skip_ad_label_button.h"
#include "chrome/browser/ui/views/overlay/toggle_camera_button.h"
#include "chrome/browser/ui/views/overlay/toggle_microphone_button.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/viz/common/frame_sinks/copy_output_request.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "content/public/browser/media_session.h"
#include "content/public/browser/overlay_window.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/media_start_stop_observer.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "media/base/media_switches.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/media_session/public/cpp/features.h"
#include "skia/ext/image_operations.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/test/draw_waiter_for_test.h"
#include "ui/display/display_switches.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/test/button_test_api.h"
#include "ui/views/view_observer.h"
#include "ui/views/widget/widget_observer.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ui/base/hit_test.h"
#endif

EvalJs;
ExecJs;
_;

namespace {

PictureInPictureWindowManagerdObservation;

class MockPictureInPictureWindowManagerObserver
    : public PictureInPictureWindowManager::Observer {};

class MockVideoPictureInPictureWindowController
    : public content::VideoPictureInPictureWindowController {};

const base::FilePath::CharType kPictureInPictureWindowSizePage[] =);

const base::FilePath::CharType kPictureInPictureVideoConferencingPage[] =);

// Determines whether |control| is visible taking into account OverlayWindow's
// custom control hiding that includes setting the size to 0x0.
bool IsOverlayWindowControlVisible(views::View* control) {}

// An observer used to notify about control visibility changes.
class ControlVisibilityObserver : views::ViewObserver {};

// A helper class to wait for widget size to change to the desired value.
class WidgetSizeChangeWaiter final : public views::WidgetObserver {};

// Waits until the given WebContents has the expected title.
void WaitForTitle(content::WebContents* web_contents,
                  const std::u16string& expected_title) {}

class OverlayControlsBecomingVisibleObserver : public views::ViewObserver {};

}  // namespace

class VideoPictureInPictureWindowControllerBrowserTest
    : public InProcessBrowserTest {};

// Checks the creation of the window controller, as well as basic window
// creation, visibility and activation.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       CreationAndVisibilityAndActivation) {}

IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       ControlsVisibility) {}

#if !BUILDFLAG(IS_CHROMEOS_ASH) && !BUILDFLAG(IS_CHROMEOS_LACROS)
class PictureInPicturePixelComparisonBrowserTest
    : public VideoPictureInPictureWindowControllerBrowserTest {};

// Plays a video in PiP. Grabs a screenshot of Picture-in-Picture window and
// verifies it's as expected.
IN_PROC_BROWSER_TEST_F(PictureInPicturePixelComparisonBrowserTest, VideoPlay) {}
#endif  // !BUILDFLAG(IS_CHROMEOS_ASH) && !BUILDFLAG(IS_CHROMEOS_LACROS)

// Tests that when an active WebContents accurately tracks whether a video
// is in Picture-in-Picture.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       TabIconUpdated) {}

// Tests that when the window is created for picture-in-picture, the callback is
// called to inform the observers about it.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       NotifyCallback) {}

// Tests that when creating a Picture-in-Picture window a size is sent to the
// caller and if the window is resized, the caller is also notified.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       ResizeEventFired) {}

// Tests that when closing a Picture-in-Picture window, the video element is
// reflected as no longer in Picture-in-Picture.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       CloseWindowWhilePlaying) {}

// Ditto, when the video isn't playing.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       CloseWindowWithoutPlaying) {}

// Tests that when closing a Picture-in-Picture window, the video element
// no longer in Picture-in-Picture can't enter Picture-in-Picture right away.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       CloseWindowCantEnterPictureInPictureAgain) {}

// Tests that when closing a Picture-in-Picture window from the Web API, the
// video element is not paused.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       CloseWindowFromWebAPIWhilePlaying) {}

// Tests that when starting a new Picture-in-Picture session from the same
// video, the video stays in Picture-in-Picture mode.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       RequestPictureInPictureTwiceFromSameVideo) {}

// Tests that when starting a new Picture-in-Picture session from the same tab,
// the previous video is no longer in Picture-in-Picture mode.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       OpenSecondPictureInPictureStopsFirst) {}

// Tests that resetting video src when video is in Picture-in-Picture session
// keep Picture-in-Picture window opened.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       ResetVideoSrcKeepsPictureInPictureWindowOpened) {}

// Tests that updating video src when video is in Picture-in-Picture session
// keep Picture-in-Picture window opened.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       UpdateVideoSrcKeepsPictureInPictureWindowOpened) {}

// Tests that changing video src to media stream when video is in
// Picture-in-Picture session keep Picture-in-Picture window opened.
IN_PROC_BROWSER_TEST_F(
    VideoPictureInPictureWindowControllerBrowserTest,
    ChangeVideoSrcToMediaStreamKeepsPictureInPictureWindowOpened) {}

// Tests that we can enter Picture-in-Picture when a video is not preloaded,
// using the metadata optimizations. This test is checking that there are no
// crashes.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       EnterMetadataPosterOptimisation) {}

// Tests that calling PictureInPictureWindowController::Close() twice has no
// side effect.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       CloseTwiceSideEffects) {}

// Checks entering Picture-in-Picture on multiple tabs, where the initial tab
// has been closed.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       PictureInPictureAfterClosingTab) {}

// Closing a tab that lost Picture-in-Picture because a new tab entered it
// should not close the current Picture-in-Picture window.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       PictureInPictureDoNotCloseAfterClosingTab) {}

// Killing an iframe that lost Picture-in-Picture because the main frame entered
// it should not close the current Picture-in-Picture window.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       PictureInPictureDoNotCloseAfterKillingFrame) {}

// Checks setting disablePictureInPicture on video just after requesting
// Picture-in-Picture doesn't result in a window opened.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       RequestPictureInPictureAfterDisablePictureInPicture) {}

// Checks that a video in Picture-in-Picture stops if its iframe is removed.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       FrameEnterLeaveClosesWindow) {}

IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       CrossOriginFrameEnterLeaveCloseWindow) {}

IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       MultipleBrowserWindowOnePIPWindow) {}

IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       EnterPictureInPictureThenNavigateAwayCloseWindow) {}

// Tests that the Picture-in-Picture state is properly updated when the window
// is closed at a system level.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       CloseWindowNotifiesController) {}

// Tests that the play/pause icon state is properly updated when a
// Picture-in-Picture is created after a reload.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       PlayPauseStateAtCreation) {}

IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       EnterUsingControllerShowsWindow) {}

IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       EnterUsingWebContentsThenUsingController) {}

IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       EnterUsingControllerThenEnterUsingWebContents) {}

// This checks that a video in Picture-in-Picture with preload none, when
// changing source willproperly update the associated media player id. This is
// checked by closing the window because the test it at a too high level to be
// able to check the actual media player id being used.
// TODO(crbug.com/40830975) Fix flakiness on ChromeOS and reenable this test.
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_PreloadNoneSrcChangeThenLoad
#else
#define MAYBE_PreloadNoneSrcChangeThenLoad
#endif
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       MAYBE_PreloadNoneSrcChangeThenLoad) {}

// Tests that opening a Picture-in-Picture window from a video in an iframe
// will not lead to a crash when the tab is closed while devtools is open.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       OpenInFrameWithDevToolsDoesNotCrash) {}

#if BUILDFLAG(IS_CHROMEOS_ASH)
// Tests that the back-to-tab, close, and resize controls move properly as
// the window changes quadrants.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       MovingQuadrantsMovesBackToTabAndResizeControls) {
  GURL test_page_url = ui_test_utils::GetTestUrl(
      base::FilePath(base::FilePath::kCurrentDirectory),
      base::FilePath(kPictureInPictureWindowSizePage));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page_url));

  content::WebContents* active_web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  ASSERT_TRUE(active_web_contents);

  SetUpWindowController(active_web_contents);
  ASSERT_TRUE(window_controller());

  ASSERT_FALSE(GetOverlayWindow());

  ASSERT_EQ(true, EvalJs(active_web_contents, "enterPictureInPicture();"));

  ASSERT_TRUE(GetOverlayWindow());
  ASSERT_TRUE(GetOverlayWindow()->IsVisible());

  // The PiP window starts in the bottom-right quadrant of the screen.
  gfx::Rect bottom_right_bounds = GetOverlayWindow()->GetBounds();
  // The relative center point of the window.
  gfx::Point center(bottom_right_bounds.width() / 2,
                    bottom_right_bounds.height() / 2);
  gfx::Point close_button_position =
      GetOverlayWindow()->close_image_position_for_testing();
  gfx::Point resize_button_position =
      GetOverlayWindow()->resize_handle_position_for_testing();

  // The close button should be in the top right corner.
  EXPECT_LT(center.x(), close_button_position.x());
  EXPECT_GT(center.y(), close_button_position.y());
  // The resize button should be in the top left corner.
  EXPECT_GT(center.x(), resize_button_position.x());
  EXPECT_GT(center.y(), resize_button_position.y());
  // The resize button hit test should start a top left resizing drag.
  EXPECT_EQ(HTTOPLEFT, GetOverlayWindow()->GetResizeHTComponent());

  // Move the window to the bottom left corner.
  gfx::Rect bottom_left_bounds(0, bottom_right_bounds.y(),
                               bottom_right_bounds.width(),
                               bottom_right_bounds.height());
  GetOverlayWindow()->SetBounds(bottom_left_bounds);
  close_button_position =
      GetOverlayWindow()->close_image_position_for_testing();
  resize_button_position =
      GetOverlayWindow()->resize_handle_position_for_testing();

  // The close button should be in the top left corner.
  EXPECT_GT(center.x(), close_button_position.x());
  EXPECT_GT(center.y(), close_button_position.y());
  // The resize button should be in the top right corner.
  EXPECT_LT(center.x(), resize_button_position.x());
  EXPECT_GT(center.y(), resize_button_position.y());
  // The resize button hit test should start a top right resizing drag.
  EXPECT_EQ(HTTOPRIGHT, GetOverlayWindow()->GetResizeHTComponent());

  // Move the window to the top right corner.
  gfx::Rect top_right_bounds(bottom_right_bounds.x(), 0,
                             bottom_right_bounds.width(),
                             bottom_right_bounds.height());
  GetOverlayWindow()->SetBounds(top_right_bounds);
  close_button_position =
      GetOverlayWindow()->close_image_position_for_testing();
  resize_button_position =
      GetOverlayWindow()->resize_handle_position_for_testing();

  // The close button should be in the top right corner.
  EXPECT_LT(center.x(), close_button_position.x());
  EXPECT_GT(center.y(), close_button_position.y());
  // The resize button should be in the bottom left corner.
  EXPECT_GT(center.x(), resize_button_position.x());
  EXPECT_LT(center.y(), resize_button_position.y());
  // The resize button hit test should start a bottom left resizing drag.
  EXPECT_EQ(HTBOTTOMLEFT, GetOverlayWindow()->GetResizeHTComponent());

  // Move the window to the top left corner.
  gfx::Rect top_left_bounds(0, 0, bottom_right_bounds.width(),
                            bottom_right_bounds.height());
  GetOverlayWindow()->SetBounds(top_left_bounds);
  close_button_position =
      GetOverlayWindow()->close_image_position_for_testing();
  resize_button_position =
      GetOverlayWindow()->resize_handle_position_for_testing();

  // The close button should be in the top right corner.
  EXPECT_LT(center.x(), close_button_position.x());
  EXPECT_GT(center.y(), close_button_position.y());
  // The resize button should be in the bottom right corner.
  EXPECT_LT(center.x(), resize_button_position.x());
  EXPECT_LT(center.y(), resize_button_position.y());
  // The resize button hit test should start a bottom right resizing drag.
  EXPECT_EQ(HTBOTTOMRIGHT, GetOverlayWindow()->GetResizeHTComponent());
}
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

// Tests that the Play/Pause button is displayed appropriately in the
// Picture-in-Picture window.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       PlayPauseButtonVisibility) {}

// Check that page visibility API events are fired when tab is hidden, shown,
// and even occluded.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       PageVisibilityEventsFired) {}

// Check that page visibility API events are fired even when video is in
// Picture-in-Picture and video playback is not disrupted.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       PageVisibilityEventsFiredWhenPictureInPicture) {}

class PictureInPictureWindowControllerPrerenderBrowserTest
    : public VideoPictureInPictureWindowControllerBrowserTest {};

// TODO(crbug.com/40902928): Reenable once Linux MSAN failure is fixed.
#if BUILDFLAG(IS_LINUX) && defined(MEMORY_SANITIZER)
#define MAYBE_EnterPipThenNavigateAwayCloseWindow
#else
#define MAYBE_EnterPipThenNavigateAwayCloseWindow
#endif
IN_PROC_BROWSER_TEST_F(PictureInPictureWindowControllerPrerenderBrowserTest,
                       MAYBE_EnterPipThenNavigateAwayCloseWindow) {}

class PictureInPictureWindowControllerFencedFrameBrowserTest
    : public VideoPictureInPictureWindowControllerBrowserTest {};

IN_PROC_BROWSER_TEST_F(PictureInPictureWindowControllerFencedFrameBrowserTest,
                       FencedFrameShouldNotCloseWindow) {}

class MediaSessionVideoPictureInPictureWindowControllerBrowserTest
    : public VideoPictureInPictureWindowControllerBrowserTest {};

// Tests that a Skip Ad button is displayed in the Picture-in-Picture window
// when Media Session Action "skipad" is handled by the website.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    SkipAdButtonVisibility) {}

// Tests that the Play/Plause button is displayed in the Picture-in-Picture
// window when Media Session actions "play" and "pause" are handled by the
// website even if video is a media stream.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    PlayPauseButtonVisibility) {}

// Tests that a Next Track button is displayed in the Picture-in-Picture window
// when Media Session Action "nexttrack" is handled by the website.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    NextTrackButtonVisibility) {}

// Tests that a Previous Track button is displayed in the Picture-in-Picture
// window when Media Session Action "previoustrack" is handled by the website.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    PreviousTrackButtonVisibility) {}

// Tests that a Next Slide button is displayed in the Picture-in-Picture window
// when Media Session Action "nextslide" is handled by the website.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    NextSlideButtonVisibility) {}

// Tests that a Previous Slide button is displayed in the Picture-in-Picture
// window when Media Session Action "previousslide" is handled by the website.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    PreviousSlideButtonVisibility) {}

// Tests that clicking the Skip Ad button in the Picture-in-Picture window
// calls the Media Session Action "skipad" handler function.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    SkipAdHandlerCalled) {}

// Tests that clicking the Play/Pause button in the Picture-in-Picture window
// calls the Media Session actions "play" and "pause" handler functions.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    // TODO(crbug.com/40878458): Re-enable this test
    DISABLED_PlayPauseHandlersCalled) {}

// Tests that clicking the Next Track button in the Picture-in-Picture window
// calls the Media Session Action "nexttrack" handler function.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    NextTrackHandlerCalled) {}

// Tests that clicking the Previous Track button in the Picture-in-Picture
// window calls the Media Session Action "previoustrack" handler function.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    PreviousTrackHandlerCalled) {}

// Tests that clicking the Next Slide button in the Picture-in-Picture window
// calls the Media Session Action "nextslide" handler function.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    NextSlideHandlerCalled) {}

// Tests that clicking the Previous Slide button in the Picture-in-Picture
// window calls the Media Session Action "previousslide" handler function.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    PreviousSlideHandlerCalled) {}

// Tests that stopping Media Sessions closes the Picture-in-Picture window.
IN_PROC_BROWSER_TEST_F(
    MediaSessionVideoPictureInPictureWindowControllerBrowserTest,
    StopMediaSessionClosesPictureInPictureWindow) {}

// Check that video with no audio that is paused when hidden resumes playback
// when it enters Picture-in-Picture.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       VideoWithNoAudioPausedWhenHiddenResumesPlayback) {}

// Tests that exiting Picture-in-Picture when the video has no source fires the
// event and resolves the callback.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       ExitFireEventAndCallbackWhenNoSource) {}

// Tests that play/pause video playback is toggled if there are no focus
// afforfances on the Picture-in-Picture window buttons when user hits space
// keyboard key.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       SpaceKeyTogglePlayPause) {}

// Test that video conferencing action buttons function correctly.
IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
                       VideoConferencingActions) {}