chromium/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_interactive_uitest.cc

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

#include <utility>

#include "base/location.h"
#include "base/numerics/safe_conversions.h"
#include "base/run_loop.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/test_timeouts.h"
#include "base/version.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/policy/policy_test_utils.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.h"
#include "chrome/browser/upgrade_detector/build_state.h"
#include "chrome/browser/upgrade_detector/upgrade_detector.h"
#include "chrome/browser/upgrade_detector/upgrade_observer.h"
#include "chrome/common/chrome_version.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "content/public/test/browser_test.h"
#include "ui/views/test/widget_activation_waiter.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/any_widget_observer.h"

namespace {

// Waits for the UpgradeDetector to broadcast to its observers that an upgrade
// has been detected.
class UpgradeRecommendedWaiter : public UpgradeObserver {
 public:
  UpgradeRecommendedWaiter() {
    UpgradeDetector::GetInstance()->AddObserver(this);
  }
  ~UpgradeRecommendedWaiter() override {
    UpgradeDetector::GetInstance()->RemoveObserver(this);
  }
  void Wait() { run_loop_.Run(); }

  // UpgradeObserver:
  void OnUpgradeRecommended() override { run_loop_.QuitWhenIdle(); }

 private:
  base::RunLoop run_loop_;
};

}  // namespace

class RelaunchNotificationControllerUiTest : public policy::PolicyTest {
 protected:
  // policy::PolicyTest:
  void SetUpOnMainThread() override {
    policy::PolicyTest::SetUpOnMainThread();

    // Configure required relaunch notifications.
    SetRelaunchNotificationPolicies();
  }

  void TearDownOnMainThread() override {
    // Disable notifications so that the timer doesn't fire during teardown.
    DisableRelaunchNotifications();

    policy::PolicyTest::TearDownOnMainThread();
  }

  // Sets the RelaunchNotification policies to show the required notification
  // at the default deadline.
  void SetRelaunchNotificationPolicies() {
    policies_.Set(policy::key::kRelaunchNotification,
                  policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_MACHINE,
                  policy::POLICY_SOURCE_PLATFORM,
                  base::Value(2),  // Required
                  nullptr);
    policies_.Set(policy::key::kRelaunchNotificationPeriod,
                  policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_MACHINE,
                  policy::POLICY_SOURCE_PLATFORM,
                  base::Value(base::saturated_cast<int>(
                      UpgradeDetector::GetDefaultHighAnnoyanceThreshold()
                          .InMilliseconds())),
                  nullptr);
    UpdateProviderPolicy(policies_);
  }

  // Disables relaunch notifications.
  void DisableRelaunchNotifications() {
    policies_.Set(policy::key::kRelaunchNotification,
                  policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_MACHINE,
                  policy::POLICY_SOURCE_PLATFORM,
                  base::Value(0),  // Disabled
                  nullptr);
    UpdateProviderPolicy(policies_);
  }

  // Simulates that an update is available.
  static void SimulateUpdate() {
    g_browser_process->GetBuildState()->SetUpdate(
        BuildState::UpdateType::kNormalUpdate,
        base::Version({CHROME_VERSION_MAJOR, CHROME_VERSION_MINOR,
                       CHROME_VERSION_BUILD, CHROME_VERSION_PATCH + 1}),
        std::nullopt);
  }

  // Triggers the annoyance level `level` to be announced to observers of
  // UpgradeDetector.
  void TriggerAnnoyanceLevel(
      UpgradeDetector::UpgradeNotificationAnnoyanceLevel level) {
    // Move the time at which the update was detected so that the original
    // detection time is, relative to the new time, at the moment that `level`
    // takes effect.
    auto* const upgrade_detector = UpgradeDetector::GetInstance();
    upgrade_detector->set_upgrade_detected_time(
        upgrade_detector->upgrade_detected_time() -
        (upgrade_detector->GetAnnoyanceLevelDeadline(level) -
         upgrade_detector->upgrade_detected_time()));

    // Force the UpgradeDetector to recompute the deadlines by reducing the
    // period by 1ms. This will also be broadcast to observers.
    policies_.Set(policy::key::kRelaunchNotificationPeriod,
                  policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_MACHINE,
                  policy::POLICY_SOURCE_PLATFORM,
                  base::Value(base::saturated_cast<int>(
                      UpgradeDetector::GetDefaultHighAnnoyanceThreshold()
                          .InMilliseconds() -
                      1)),
                  nullptr);
    UpdateProviderPolicy(policies_);
  }

  // Closes `dialog` and waits for it to be destroyed.
  static void CloseDialog(RelaunchRequiredDialogView* dialog) {
    views::test::WidgetDestroyedWaiter waiter(dialog->GetWidget());
    std::exchange(dialog, nullptr)->CancelDialog();
    waiter.Wait();
  }

  // Minimizes `browser_view` and waits for it to be deactivated.
  static void MinimizeBrowser(BrowserView* browser_view) {
    browser_view->Minimize();
    views::test::WaitForWidgetActive(browser_view->GetWidget(),
                                     /*active=*/false);
    // Pump all pending UI events so that the window manager isn't racing with
    // the test.
    base::RunLoop().RunUntilIdle();
  }

  // Sets the RelaunchNotificationPeriod so that the deadline is `delta` in
  // the future. Returns the new deadline.
  base::Time AdjustPeriodToDeadlineIn(base::TimeDelta delta) {
    UpgradeRecommendedWaiter waiter;
    const base::Time deadline = base::Time::Now() + delta;
    const base::TimeDelta period =
        deadline - UpgradeDetector::GetInstance()->upgrade_detected_time();
    policies_.Set(
        policy::key::kRelaunchNotificationPeriod,
        policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_MACHINE,
        policy::POLICY_SOURCE_PLATFORM,
        base::Value(base::saturated_cast<int>(period.InMilliseconds())),
        nullptr);
    UpdateProviderPolicy(policies_);
    waiter.Wait();
    return deadline;
  }

 private:
  policy::PolicyMap policies_;
};

// Tests that reactivating a browser window after the deadline has passed does
// not show a negative delta.
// Fails on mac64; see https://crbug.com/1462892.
IN_PROC_BROWSER_TEST_F(RelaunchNotificationControllerUiTest,
                       ReactivateAfterDeadline) {
  // Make sure a browser window is active.
  auto* const browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  ASSERT_TRUE(browser_view->IsActive());
  ASSERT_EQ(chrome::FindBrowserWithActiveWindow(), browser());

  // Simulate an update and wait for the notification to show.
  RelaunchRequiredDialogView* dialog = nullptr;
  {
    views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
                                         "RelaunchRequiredDialog");
    SimulateUpdate();
    TriggerAnnoyanceLevel(UpgradeDetector::UPGRADE_ANNOYANCE_ELEVATED);
    auto* widget = waiter.WaitIfNeededAndGet();
    ASSERT_TRUE(widget);
    dialog = RelaunchRequiredDialogView::FromWidget(widget);
  }

  // Dismiss the notification and wait for its destruction.
  CloseDialog(std::exchange(dialog, nullptr));

  // Deactivate the browser window.
  MinimizeBrowser(browser_view);
  ASSERT_FALSE(chrome::FindBrowserWithActiveWindow());

  // The code below assumes that `action_timeout` is greater than 2.5 seconds.
  ASSERT_GT(TestTimeouts::action_timeout(), base::Milliseconds(2500));

  // Reduce the relaunch notification period to a deadline that is just in the
  // future. The browser is not active, so the controller will install an
  // observer and wait for it to become active. The controller caches this
  // deadline.
  auto deadline = AdjustPeriodToDeadlineIn(TestTimeouts::action_timeout() / 5);

  // Increase the relaunch notification period a bit. In the buggy case, the
  // controller does not update the cached value of the deadline.
  AdjustPeriodToDeadlineIn(TestTimeouts::action_timeout() / 2.5);

  // Advance 1/2 second past the first revised deadline. This lets the clock
  // move past the bad deadline cached by the controller for the dialog, but is
  // still before the true relaunch deadline. It is significant that the clock
  // is at least 1/2 second ahead so that the rounded delta to be shown in the
  // UX is less than zero in the buggy case.
  {
    base::RunLoop run_loop;
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE, run_loop.QuitClosure(),
        (deadline + base::Milliseconds(500)) - base::Time::Now());
    run_loop.Run();
  }

  // Activate the browser window and wait for the notification to show.
  {
    views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
                                         "RelaunchRequiredDialog");
    browser_view->Restore();
    auto* widget = waiter.WaitIfNeededAndGet();
    ASSERT_TRUE(widget);
    dialog = RelaunchRequiredDialogView::FromWidget(widget);
  }

  ASSERT_TRUE(dialog);
  ASSERT_GE(dialog->deadline(), base::Time::Now());
}