chromium/chrome/browser/ui/views/apps/chrome_native_app_window_views_aura_ash_browsertest.cc

// Copyright 2017 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/apps/chrome_native_app_window_views_aura_ash.h"

#include <memory>

#include "ash/public/cpp/split_view_test_api.h"
#include "ash/public/cpp/tablet_mode.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "build/build_config.h"
#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
#include "chrome/browser/apps/platform_apps/app_window_interactive_uitest_base.h"
#include "chrome/browser/ash/login/test/local_state_mixin.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chromeos/ash/components/login/login_state/scoped_test_public_session_login_state.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/frame/immersive/immersive_fullscreen_controller.h"
#include "components/user_manager/user_manager.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/test/extension_test_message_listener.h"
#include "ui/aura/window.h"
#include "ui/base/ui_base_types.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/views/view_observer.h"
#include "ui/wm/core/window_util.h"

namespace {

class ViewBoundsChangeWaiter : public views::ViewObserver {
 public:
  ViewBoundsChangeWaiter(const ViewBoundsChangeWaiter&) = delete;
  ViewBoundsChangeWaiter& operator=(const ViewBoundsChangeWaiter&) = delete;

  static void VerifyY(views::View* view, int y) {
    if (y != view->bounds().y())
      ViewBoundsChangeWaiter(view).run_loop_.Run();

    EXPECT_EQ(y, view->bounds().y());
  }

 private:
  explicit ViewBoundsChangeWaiter(views::View* view) {
    observation_.Observe(view);
  }
  ~ViewBoundsChangeWaiter() override = default;

  // ViewObserver:
  void OnViewBoundsChanged(views::View* view) override { run_loop_.Quit(); }

  base::RunLoop run_loop_;

  base::ScopedObservation<views::View, views::ViewObserver> observation_{this};
};

}  // namespace

class ChromeNativeAppWindowViewsAuraAshBrowserTest
    : public AppWindowInteractiveTest {
 public:
  ChromeNativeAppWindowViewsAuraAshBrowserTest() = default;

  ChromeNativeAppWindowViewsAuraAshBrowserTest(
      const ChromeNativeAppWindowViewsAuraAshBrowserTest&) = delete;
  ChromeNativeAppWindowViewsAuraAshBrowserTest& operator=(
      const ChromeNativeAppWindowViewsAuraAshBrowserTest&) = delete;

  ~ChromeNativeAppWindowViewsAuraAshBrowserTest() override = default;

 protected:
  void InitWindow() { app_window_ = CreateTestAppWindow("{}"); }

  bool IsImmersiveActive() {
    return window()->widget()->GetNativeWindow()->GetProperty(
        chromeos::kImmersiveIsActive);
  }

  ChromeNativeAppWindowViewsAuraAsh* window() {
    return static_cast<ChromeNativeAppWindowViewsAuraAsh*>(
        GetFirstAppWindow()->GetBaseWindow());
  }

  std::unique_ptr<ExtensionTestMessageListener>
  LaunchPlatformAppWithFocusedWindow() {
    std::unique_ptr<ExtensionTestMessageListener> launched_listener =
        std::make_unique<ExtensionTestMessageListener>(
            "Launched", ReplyBehavior::kWillReply);
    LoadAndLaunchPlatformApp("leave_fullscreen", launched_listener.get());

    // We start by making sure the window is actually focused.
    EXPECT_TRUE(ui_test_utils::ShowAndFocusNativeWindow(
        GetFirstAppWindow()->GetNativeWindow()));
    return launched_listener;
  }

  // When receiving the reply, the application will try to go fullscreen using
  // the Window API but there is no synchronous way to know if that actually
  // succeeded. Also, failure will not be notified. A failure case will only be
  // known with a timeout.
  void WaitFullscreenChange(ExtensionTestMessageListener* launched_listener) {
    FullscreenChangeWaiter fullscreen_changed(
        GetFirstAppWindow()->GetBaseWindow());
    launched_listener->Reply("window");
    fullscreen_changed.Wait();
  }

  // Because the DOM way to go fullscreen requires user gesture, we simulate a
  // key event to get the window to enter fullscreen mode. The reply will
  // make the window listen for the key event. The reply will be sent to the
  // renderer process before the keypress and should be received in that order.
  // When receiving the key event, the application will try to go fullscreen
  // using the Window API but there is no synchronous way to know if that
  // actually succeeded. Also, failure will not be notified. A failure case will
  // only be known with a timeout.
  void WaitFullscreenChangeUntilKeyFocus(
      ExtensionTestMessageListener* launched_listener) {
    launched_listener->Reply("dom");

    FullscreenChangeWaiter fs_changed(GetFirstAppWindow()->GetBaseWindow());
    WaitUntilKeyFocus();
    ASSERT_TRUE(SimulateKeyPress(ui::VKEY_A));
    fs_changed.Wait();
  }

  // Verifies that changing the fullscreen |type| sets the expected shelf
  // visibility. |is_shelf_hidden| expects the shelf to hide when the Chrome App
  // is in fullscreen, otherwise the shelf will autohide.
  void VerifyShelfHiddenForFullscreenType(
      extensions::AppWindow::FullscreenType type,
      bool is_shelf_hidden) {
    app_window_->SetFullscreen(type, /*enabled=*/true);
    EXPECT_EQ(is_shelf_hidden,
              window()->widget()->GetNativeWindow()->GetProperty(
                  chromeos::kHideShelfWhenFullscreenKey));
    app_window_->SetFullscreen(type, /*enabled=*/false);
  }

  // Verifies the shelf visibility for all fullscreen types.
  void VerifyShelfBehaviorWhenFullscreen() {
    InitWindow();
    ASSERT_TRUE(window());
    EXPECT_TRUE(window()->widget()->GetNativeWindow()->GetProperty(
        chromeos::kHideShelfWhenFullscreenKey));

    VerifyShelfHiddenForFullscreenType(
        extensions::AppWindow::FULLSCREEN_TYPE_WINDOW_API,
        /*is_shelf_hidden=*/true);
    VerifyShelfHiddenForFullscreenType(
        extensions::AppWindow::FULLSCREEN_TYPE_HTML_API,
        /*is_shelf_hidden=*/true);
    VerifyShelfHiddenForFullscreenType(
        extensions::AppWindow::FULLSCREEN_TYPE_FORCED,
        /*is_shelf_hidden=*/true);
    VerifyShelfHiddenForFullscreenType(
        extensions::AppWindow::FULLSCREEN_TYPE_OS, /*is_shelf_hidden=*/false);
  }

  raw_ptr<extensions::AppWindow, DanglingUntriaged> app_window_ = nullptr;
};

class ChromeNativeAppWindowViewsAuraPublicSessionAshBrowserTest
    : public ChromeNativeAppWindowViewsAuraAshBrowserTest,
      public ash::LocalStateMixin::Delegate {
 public:
  void SetUpLocalState() override {
    // Until ScopedTestPublicSessionLoginState is created, the setup runs in a
    // regular user session. Marking another user as the owner prevents the
    // current user from taking ownership and overriding the public session
    // mode.
    user_manager::UserManager::Get()->RecordOwner(
        AccountId::FromUserEmail("[email protected]"));
  }

  void SetUpOnMainThread() override {
    ChromeNativeAppWindowViewsAuraAshBrowserTest::SetUpOnMainThread();
    // Emulate public session.
    login_state_ = std::make_unique<ash::ScopedTestPublicSessionLoginState>();
  }

  void TearDownOnMainThread() override {
    login_state_.reset();
    ChromeNativeAppWindowViewsAuraAshBrowserTest::TearDownOnMainThread();
  }

 private:
  ash::LocalStateMixin local_state_mixin_{&mixin_host_, this};
  std::unique_ptr<ash::ScopedTestPublicSessionLoginState> login_state_;
};

// Verify that immersive mode is enabled or disabled as expected.
IN_PROC_BROWSER_TEST_F(ChromeNativeAppWindowViewsAuraAshBrowserTest,
                       ImmersiveWorkFlow) {
  InitWindow();
  ASSERT_TRUE(window());
  EXPECT_FALSE(IsImmersiveActive());
  const int frame_height =
      chromeos::features::IsRoundedWindowsEnabled() ? 40 : 32;

  views::ClientView* client_view =
      window()->widget()->non_client_view()->client_view();
  EXPECT_EQ(frame_height, client_view->bounds().y());

  // Verify that when fullscreen is toggled on, immersive mode is enabled and
  // that when fullscreen is toggled off, immersive mode is disabled.
  app_window_->OSFullscreen();
  EXPECT_TRUE(IsImmersiveActive());
  ViewBoundsChangeWaiter::VerifyY(client_view, 0);

  app_window_->Restore();
  EXPECT_FALSE(IsImmersiveActive());
  ViewBoundsChangeWaiter::VerifyY(client_view, frame_height);

  // Verify that since the auto hide title bars in tablet mode feature turned
  // on, immersive mode is enabled once tablet mode is entered, and disabled
  // once tablet mode is exited.
  ash::ShellTestApi().SetTabletModeEnabledForTest(true);
  EXPECT_TRUE(IsImmersiveActive());
  ViewBoundsChangeWaiter::VerifyY(client_view, 0);

  ash::ShellTestApi().SetTabletModeEnabledForTest(false);
  EXPECT_FALSE(IsImmersiveActive());
  ViewBoundsChangeWaiter::VerifyY(client_view, frame_height);

  // Verify that the window was fullscreened before entering tablet mode, it
  // will remain fullscreened after exiting tablet mode.
  app_window_->OSFullscreen();
  EXPECT_TRUE(IsImmersiveActive());
  ash::ShellTestApi().SetTabletModeEnabledForTest(true);
  EXPECT_TRUE(IsImmersiveActive());
  ash::ShellTestApi().SetTabletModeEnabledForTest(false);
  EXPECT_TRUE(IsImmersiveActive());
  app_window_->Restore();

  // Verify that minimized windows do not have immersive mode enabled.
  app_window_->Minimize();
  EXPECT_FALSE(IsImmersiveActive());
  ash::ShellTestApi().SetTabletModeEnabledForTest(true);
  EXPECT_FALSE(IsImmersiveActive());
  window()->Restore();
  EXPECT_TRUE(IsImmersiveActive());
  app_window_->Minimize();
  EXPECT_FALSE(IsImmersiveActive());
  ash::ShellTestApi().SetTabletModeEnabledForTest(false);
  EXPECT_FALSE(IsImmersiveActive());

  // Verify that activation change should not change the immersive
  // state.
  window()->Show();
  app_window_->OSFullscreen();
  EXPECT_TRUE(IsImmersiveActive());
  ::wm::DeactivateWindow(window()->GetNativeWindow());
  EXPECT_TRUE(IsImmersiveActive());
  ::wm::ActivateWindow(window()->GetNativeWindow());
  EXPECT_TRUE(IsImmersiveActive());

  CloseAppWindow(app_window_);
}

// Verifies that apps in immersive fullscreen will have a restore state of
// maximized.
IN_PROC_BROWSER_TEST_F(ChromeNativeAppWindowViewsAuraAshBrowserTest,
                       ImmersiveModeFullscreenRestoreType) {
  InitWindow();
  ASSERT_TRUE(window());

  app_window_->OSFullscreen();
  EXPECT_EQ(ui::SHOW_STATE_NORMAL, window()->GetRestoredState());
  ash::ShellTestApi().SetTabletModeEnabledForTest(true);
  EXPECT_TRUE(window()->IsFullscreen());
  EXPECT_EQ(ui::SHOW_STATE_NORMAL, window()->GetRestoredState());
  ash::ShellTestApi().SetTabletModeEnabledForTest(false);
  EXPECT_EQ(ui::SHOW_STATE_NORMAL, window()->GetRestoredState());

  CloseAppWindow(app_window_);
}

// Verify that immersive mode stays disabled when entering tablet mode in
// forced fullscreen mode (e.g. when running in a kiosk session).
IN_PROC_BROWSER_TEST_F(ChromeNativeAppWindowViewsAuraAshBrowserTest,
                       NoImmersiveModeWhenForcedFullscreen) {
  InitWindow();
  ASSERT_TRUE(window());

  app_window_->ForcedFullscreen();

  ash::ShellTestApi().SetTabletModeEnabledForTest(true);
  EXPECT_FALSE(IsImmersiveActive());
  ash::ShellTestApi().SetTabletModeEnabledForTest(false);
  EXPECT_FALSE(IsImmersiveActive());
}

// Verify that the shelf behavior when requesting fullscreen is consistent
// for regular user sessions.
IN_PROC_BROWSER_TEST_F(ChromeNativeAppWindowViewsAuraAshBrowserTest,
                       ShelfBehaviorWhenFullscreenForRegularUserSessions) {
  VerifyShelfBehaviorWhenFullscreen();
}

// Verify that the shelf behavior when requesting fullscreen is consistent
// for managed guest sessions.
IN_PROC_BROWSER_TEST_F(
    ChromeNativeAppWindowViewsAuraPublicSessionAshBrowserTest,
    ShelfBehaviorWhenFullscreenForManagedGuestSessions) {
  VerifyShelfBehaviorWhenFullscreen();
}

// Verify that immersive mode stays disabled in the public session, no matter
// that the app is in a normal window or fullscreen mode.
IN_PROC_BROWSER_TEST_F(
    ChromeNativeAppWindowViewsAuraPublicSessionAshBrowserTest,
    PublicSessionNoImmersiveModeWhenFullscreen) {
  InitWindow();
  ASSERT_TRUE(window());
  EXPECT_FALSE(IsImmersiveActive());

  app_window_->SetFullscreen(extensions::AppWindow::FULLSCREEN_TYPE_HTML_API,
                             true);

  EXPECT_FALSE(IsImmersiveActive());
}

// Verifies that apps in clamshell mode with immersive fullscreen enabled will
// correctly exit immersive mode if exit fullscreen.
IN_PROC_BROWSER_TEST_F(ChromeNativeAppWindowViewsAuraAshBrowserTest,
                       RestoreImmersiveMode) {
  InitWindow();
  ASSERT_TRUE(window());

  // Should not disable immersive fullscreen in tablet mode if |window| exits
  // fullscreen.
  EXPECT_FALSE(window()->IsFullscreen());
  app_window_->OSFullscreen();
  EXPECT_EQ(ui::SHOW_STATE_NORMAL, window()->GetRestoredState());
  EXPECT_TRUE(window()->IsFullscreen());
  EXPECT_TRUE(IsImmersiveActive());
  ash::ShellTestApi().SetTabletModeEnabledForTest(true);
  EXPECT_TRUE(window()->IsFullscreen());
  EXPECT_EQ(ui::SHOW_STATE_NORMAL, window()->GetRestoredState());

  window()->Restore();
  // Restoring a window inside tablet mode should deactivate fullscreen, but not
  // disable immersive mode.
  EXPECT_FALSE(window()->IsFullscreen());
  ASSERT_TRUE(IsImmersiveActive());

  // Immersive fullscreen should be disabled if window exits fullscreen in
  // clamshell mode.
  ash::ShellTestApi().SetTabletModeEnabledForTest(false);
  app_window_->OSFullscreen();
  // Note that windows that are fullscreened before entering tablet mode are
  // maximized when leaving it.
  EXPECT_EQ(ui::SHOW_STATE_MAXIMIZED, window()->GetRestoredState());
  EXPECT_TRUE(window()->IsFullscreen());

  window()->Restore();
  EXPECT_FALSE(IsImmersiveActive());

  CloseAppWindow(app_window_);
}

// Ensures that JS-activated fullscreen doesn't trigger the immersive mode or
// show a bubble except the public session. (Window API)
IN_PROC_BROWSER_TEST_F(ChromeNativeAppWindowViewsAuraAshBrowserTest,
                       NoImmersiveOrBubbleOutsidePublicSessionWindow) {
  std::unique_ptr<ExtensionTestMessageListener> launched_listener =
      LaunchPlatformAppWithFocusedWindow();
  WaitFullscreenChange(launched_listener.get());

  EXPECT_FALSE(window()->IsImmersiveModeEnabled());
  EXPECT_FALSE(window()->exclusive_access_bubble_);
}

// Ensures that JS-activated fullscreen doesn't trigger the immersive mode or
// show a bubble except the public session. (DOM)
IN_PROC_BROWSER_TEST_F(ChromeNativeAppWindowViewsAuraAshBrowserTest,
                       NoImmersiveOrBubbleOutsidePublicSessionDom) {
  std::unique_ptr<ExtensionTestMessageListener> launched_listener =
      LaunchPlatformAppWithFocusedWindow();
  WaitFullscreenChangeUntilKeyFocus(launched_listener.get());

  EXPECT_FALSE(window()->IsImmersiveModeEnabled());
  EXPECT_FALSE(window()->exclusive_access_bubble_);
}

// Ensures that JS-activated fullscreen in the Public session doesn't trigger
// the immersive mode, but shows a bubble to guide users how to exit the
// fullscreen mode under different conditions. (Window API)
IN_PROC_BROWSER_TEST_F(
    ChromeNativeAppWindowViewsAuraPublicSessionAshBrowserTest,
    BubbleInsidePublicSessionWindow) {
  std::unique_ptr<ExtensionTestMessageListener> launched_listener =
      LaunchPlatformAppWithFocusedWindow();
  WaitFullscreenChange(launched_listener.get());

  EXPECT_FALSE(window()->IsImmersiveModeEnabled());
  EXPECT_TRUE(window()->exclusive_access_bubble_);
}

// Ensures that JS-activated fullscreen in the Public session doesn't trigger
// the immersive mode, but shows a bubble to guide users how to exit the
// fullscreen mode under different conditions. (DOM)
IN_PROC_BROWSER_TEST_F(
    ChromeNativeAppWindowViewsAuraPublicSessionAshBrowserTest,
    BubbleInsidePublicSessionDom) {
  std::unique_ptr<ExtensionTestMessageListener> launched_listener =
      LaunchPlatformAppWithFocusedWindow();
  WaitFullscreenChangeUntilKeyFocus(launched_listener.get());

  EXPECT_FALSE(window()->IsImmersiveModeEnabled());
  EXPECT_TRUE(window()->exclusive_access_bubble_);
}

// Tests the auto positioning logic of created windows does not apply to apps
// which specify their own positions.
IN_PROC_BROWSER_TEST_F(ChromeNativeAppWindowViewsAuraAshBrowserTest,
                       UserGivenBoundsAreRespected) {
  display::DisplayManager* display_manager =
      ash::ShellTestApi().display_manager();
  display::test::DisplayManagerTestApi(display_manager)
      .UpdateDisplay("800x700");

  const extensions::Extension* extension =
      LoadAndLaunchPlatformApp("launch", "Launched");

  // This is the default size apps are if no window or content specifications
  // are given.
  const gfx::Size default_size(512, 384);

  // Create an app with no window or content specifications. Use no frame for
  // simpler calculations.
  extensions::AppWindow::CreateParams params;
  params.frame = extensions::AppWindow::FRAME_NONE;
  extensions::AppWindow* app_window =
      CreateAppWindowFromParams(browser()->profile(), extension, params);

  // Test that the window is centered within the work area.
  gfx::Rect expected_bounds = display_manager->GetDisplayAt(0).work_area();
  expected_bounds.ClampToCenteredSize(default_size);
  EXPECT_EQ(expected_bounds,
            app_window->GetNativeWindow()->GetBoundsInScreen());
  CloseAppWindow(app_window);

  // Create an app with content specifications. The window is placed where the
  // user specified.
  {
    const gfx::Rect specified_bounds(10, 10, 600, 400);
    extensions::AppWindow::BoundsSpecification content_spec;
    content_spec.bounds = specified_bounds;
    params.content_spec = content_spec;
    app_window =
        CreateAppWindowFromParams(browser()->profile(), extension, params);
    EXPECT_EQ(specified_bounds,
              app_window->GetNativeWindow()->GetBoundsInScreen());
  }
  CloseAppWindow(app_window);

  // Create an app with content specifications on the secondary display. The
  // window is placed where the user specified.
  display::test::DisplayManagerTestApi(display_manager)
      .UpdateDisplay("800x700,800+0-800x700");
  {
    const gfx::Rect specified_bounds(810, 10, 600, 400);
    extensions::AppWindow::BoundsSpecification content_spec;
    content_spec.bounds = specified_bounds;
    params.content_spec = content_spec;
    app_window =
        CreateAppWindowFromParams(browser()->profile(), extension, params);
    EXPECT_EQ(specified_bounds,
              app_window->GetNativeWindow()->GetBoundsInScreen());
  }
  CloseAppWindow(app_window);
}

// Tests that opening a chrome app window when a window is already snapped will
// snap it as well, even if the window is meant to be created maximized.
IN_PROC_BROWSER_TEST_F(ChromeNativeAppWindowViewsAuraAshBrowserTest,
                       OpeningDefaultMaximizedWindowInSplitview) {
  ash::TabletMode::Get()->SetEnabledForTest(true);

  const extensions::Extension* extension =
      LoadAndLaunchPlatformApp("launch", "Launched");

  extensions::AppWindow::CreateParams params;
  extensions::AppWindow* app1_window =
      CreateAppWindowFromParams(browser()->profile(), extension, params);

  ash::SplitViewTestApi split_view_test_api;
  split_view_test_api.SnapWindow(app1_window->GetNativeWindow(),
                                 ash::SnapPosition::kPrimary);
  ASSERT_EQ(app1_window->GetNativeWindow(),
            split_view_test_api.GetPrimaryWindow());

  // Open a second app window that should be created maximized. It should be
  // snapped.
  params.state = ui::SHOW_STATE_MAXIMIZED;
  extensions::AppWindow* app2_window =
      CreateAppWindowFromParams(browser()->profile(), extension, params);
  ASSERT_EQ(app2_window->GetNativeWindow(),
            split_view_test_api.GetSecondaryWindow());
}