chromium/content/browser/media/capture/screen_capture_kit_fullscreen_module_unittest.mm

// 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 "content/browser/media/capture/screen_capture_kit_fullscreen_module.h"

#include "base/task/bind_post_task.h"
#import "base/task/single_thread_task_runner.h"
#include "base/test/task_environment.h"
#include "base/timer/timer.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#include "ui/gfx/geometry/rect.h"

namespace content {
namespace {
MATCHER_P(MatchesScWindow, ID, "") {
  *result_listener << "with ID " << arg.windowID;
  return arg.windowID == ID;
}

static NSString* const kEmptyString = @"";

static NSString* const kApplicationNameFooBar = @"FooBar";
static NSString* const kApplicationNameKeynote = @"Keynote";
static NSString* const kApplicationNameLibreOffice = @"LibreOffice";
static NSString* const kApplicationNameOpenOffice = @"OpenOffice";
static NSString* const kApplicationNamePowerPoint = @"Microsoft PowerPoint";

constexpr gfx::Rect kDisplayPrimary(0, 0, 1920, 1080);
constexpr gfx::Rect kDisplaySecondary(-1920, 10, 1920, 1080);

constexpr gfx::Rect kFrameOther(140, 170, 600, 400);
constexpr gfx::Rect kFrameEditor(10, 20, 1280, 720);
constexpr gfx::Rect kFrameSlideshow = kDisplayPrimary;
constexpr gfx::Rect kFramePresentersView = kDisplaySecondary;

enum class Application {
  kFoobar = 0,
  kPowerPoint = 1,
  kKeynote = 2,
  kOpenOffice = 3,
  kLibreOffice = 4,
  kSize
};

enum class Mode {
  kOther = 0,
  kEditor = 1,
  kPresentersView = 2,
  kSlideshow = 3,
};

class WindowConfig {
 public:
  WindowConfig(Application application,
               Mode mode,
               int doc_index,
               bool on_screen)
      : application_(application),
        mode_(mode),
        doc_index_(doc_index),
        on_screen_(on_screen) {}

  pid_t process_id() const {
    return static_cast<int>(application_) * 11 + 1234;
  }

  NSString* application_name() const {
    switch (application_) {
      case Application::kFoobar:
        return kApplicationNameFooBar;
      case Application::kPowerPoint:
        return kApplicationNamePowerPoint;
      case Application::kKeynote:
        return kApplicationNameKeynote;
      case Application::kOpenOffice:
        return kApplicationNameOpenOffice;
      case Application::kLibreOffice:
        return kApplicationNameLibreOffice;
      default:
        return kEmptyString;
    }
  }

  NSString* window_title() const {
    switch (application_) {
      case Application::kFoobar:
        return kEmptyString;
      case Application::kPowerPoint: {
        NSString* doc_name =
            [NSString stringWithFormat:@"Presentation%d", doc_index_];
        switch (mode_) {
          case Mode::kEditor:
            return doc_name;
          case Mode::kPresentersView:
            return [NSString
                stringWithFormat:@"PowerPoint Presenter View - [%@]", doc_name];
          case Mode::kSlideshow:
            return [NSString
                stringWithFormat:@"PowerPoint Slide Show - [%@]", doc_name];
          default:
            return kEmptyString;
        }
      }
      case Application::kKeynote: {
        NSString* doc_name =
            [NSString stringWithFormat:@"Untitled%d", doc_index_];
        return doc_name;
      }
      case Application::kOpenOffice: {
        NSString* doc_name =
            [NSString stringWithFormat:@"Untitled %d", doc_index_];
        switch (mode_) {
          case Mode::kEditor:
          case Mode::kPresentersView:
            return [NSString
                stringWithFormat:@"%@ - OpenOffice Impress", doc_name];
          case Mode::kSlideshow:
          default:
            return kEmptyString;
        }
      }
      default:
        return kEmptyString;
    }
  }

  int window_layer() const { return static_cast<int>(mode_); }

  bool on_screen() const { return on_screen_; }
  void set_on_screen(bool on_screen) { on_screen_ = on_screen; }

  CGRect frame() const {
    switch (mode_) {
      case Mode::kOther:
        return kFrameOther.ToCGRect();
      case Mode::kEditor:
        return kFrameEditor.ToCGRect();
      case Mode::kPresentersView:
        return kFramePresentersView.ToCGRect();
      case Mode::kSlideshow:
        return kFrameSlideshow.ToCGRect();
      default:
        NOTREACHED_IN_MIGRATION();
    }
  }

  bool active() const { return active_; }
  void set_active(bool active) { active_ = active; }

 private:
  Application application_;
  Mode mode_;
  int doc_index_;
  bool on_screen_;
  bool active_ = true;
};

SCDisplay* API_AVAILABLE(macos(12.3)) CreateSCDisplay(CGRect frame) {
  id display = OCMClassMock([SCDisplay class]);
  OCMStub([display frame]).andReturn(frame);
  return display;
}

}  // namespace

class API_AVAILABLE(macos(12.3)) MockResetStreamInterface
    : public ScreenCaptureKitResetStreamInterface {
 public:
  MOCK_METHOD1(ResetStreamTo, void(SCWindow* window));
};

class SCKFullscreenModuleTest : public testing::Test {
 public:
  SCKFullscreenModuleTest() = default;

  void SetUp() override {}
  SCWindow* API_AVAILABLE(macos(12.3)) AddWindow(WindowConfig window_config) {
    windows_.push_back(window_config);
    return CreateSCWindow(window_config, windows_.size());
  }

  void API_AVAILABLE(macos(12.3)) getShareableContentMock(
      ScreenCaptureKitFullscreenModule::ContentHandler handler) {
    NSArray* windows = [NSArray array];
    for (size_t i = 0; i < windows_.size(); ++i) {
      if (windows_[i].active()) {
        windows =
            [windows arrayByAddingObject:CreateSCWindow(windows_[i], i + 1)];
      }
    }

    NSArray* displays = @[
      CreateSCDisplay(kDisplayPrimary.ToCGRect()),
      CreateSCDisplay(kDisplaySecondary.ToCGRect())
    ];

    id content = OCMClassMock([SCShareableContent class]);

    OCMStub([content windows]).andReturn(windows);
    OCMStub([content displays]).andReturn(displays);

    std::move(handler).Run(content);
  }

 protected:
  SCWindow* API_AVAILABLE(macos(12.3))
      CreateSCWindow(WindowConfig config, CGWindowID window_id) const {
    id window = OCMClassMock([SCWindow class]);
    id owning_application = OCMClassMock([SCRunningApplication class]);

    OCMStub([owning_application applicationName])
        .andReturn(config.application_name());
    OCMStub([owning_application processID]).andReturn(config.process_id());
    OCMStub([window owningApplication]).andReturn(owning_application);
    OCMStub([window title]).andReturn(config.window_title());
    OCMStub([window windowID]).andReturn(window_id);
    OCMStub([window windowLayer]).andReturn(config.window_layer());
    OCMStub([window frame]).andReturn(config.frame());
    OCMStub([window isOnScreen]).andReturn(config.on_screen());
    return window;
  }

  void SetWindowOnScreen(CGWindowID id, bool on_screen) {
    windows_[id - 1].set_on_screen(on_screen);
  }

  void SetWindowActive(CGWindowID id, bool active) {
    windows_[id - 1].set_active(active);
  }

  void StepForward(base::TimeDelta delta, int steps) {
    for (int i = 0; i < steps; ++i) {
      task_environment_.FastForwardBy(delta);
    }
  }

  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  std::vector<WindowConfig> windows_;
};

TEST_F(SCKFullscreenModuleTest, PowerPointInitialization) {
  if (@available(macOS 13.1, *)) {
    MockResetStreamInterface reset_stream_interface;

    SCWindow* editor_window = AddWindow(
        {Application::kPowerPoint, Mode::kEditor, 0, /*on_screen=*/true});

    std::unique_ptr<ScreenCaptureKitFullscreenModule> fullscreen_module =
        MaybeCreateScreenCaptureKitFullscreenModule(
            base::SingleThreadTaskRunner::GetCurrentDefault(),
            reset_stream_interface, editor_window);
    EXPECT_TRUE(fullscreen_module);
    EXPECT_EQ(fullscreen_module->get_mode(),
              ScreenCaptureKitFullscreenModule::Mode::kPowerPoint);
  }
}

TEST_F(SCKFullscreenModuleTest, OpenOfficeInitialization) {
  if (@available(macOS 13.1, *)) {
    MockResetStreamInterface reset_stream_interface;

    SCWindow* editor_window = AddWindow(
        {Application::kOpenOffice, Mode::kEditor, 0, /*on_screen=*/true});

    std::unique_ptr<ScreenCaptureKitFullscreenModule> fullscreen_module =
        MaybeCreateScreenCaptureKitFullscreenModule(
            base::SingleThreadTaskRunner::GetCurrentDefault(),
            reset_stream_interface, editor_window);
    EXPECT_TRUE(fullscreen_module);
    EXPECT_EQ(fullscreen_module->get_mode(),
              ScreenCaptureKitFullscreenModule::Mode::kOpenOffice);
  }
}

TEST_F(SCKFullscreenModuleTest, KeynoteInitialization) {
  if (@available(macOS 13.1, *)) {
    MockResetStreamInterface reset_stream_interface;

    SCWindow* editor_window = AddWindow(
        {Application::kKeynote, Mode::kEditor, 0, /*on_screen=*/true});

    std::unique_ptr<ScreenCaptureKitFullscreenModule> fullscreen_module =
        MaybeCreateScreenCaptureKitFullscreenModule(
            base::SingleThreadTaskRunner::GetCurrentDefault(),
            reset_stream_interface, editor_window);
    EXPECT_TRUE(fullscreen_module);
    EXPECT_EQ(fullscreen_module->get_mode(),
              ScreenCaptureKitFullscreenModule::Mode::kKeynote);
  }
}

TEST_F(SCKFullscreenModuleTest, LibreOfficeInitializationFails) {
  if (@available(macOS 13.1, *)) {
    MockResetStreamInterface reset_stream_interface;

    SCWindow* editor_window = AddWindow(
        {Application::kLibreOffice, Mode::kEditor, 0, /*on_screen=*/true});

    std::unique_ptr<ScreenCaptureKitFullscreenModule> fullscreen_module =
        MaybeCreateScreenCaptureKitFullscreenModule(
            base::SingleThreadTaskRunner::GetCurrentDefault(),
            reset_stream_interface, editor_window);
    EXPECT_FALSE(fullscreen_module);
  }
}

TEST_F(SCKFullscreenModuleTest, FooBarInitializationFails) {
  if (@available(macOS 13.1, *)) {
    MockResetStreamInterface reset_stream_interface;

    SCWindow* editor_window =
        AddWindow({Application::kFoobar, Mode::kEditor, 0, /*on_screen=*/true});

    std::unique_ptr<ScreenCaptureKitFullscreenModule> fullscreen_module =
        MaybeCreateScreenCaptureKitFullscreenModule(
            base::SingleThreadTaskRunner::GetCurrentDefault(),
            reset_stream_interface, editor_window);
    EXPECT_FALSE(fullscreen_module);
  }
}

// Test that the callback is run.
// Add content when calling the callback handler.
TEST_F(SCKFullscreenModuleTest, DetectFullscreenWindowPowerPoint) {
  if (@available(macOS 13.1, *)) {
    MockResetStreamInterface reset_stream_interface;

    // Add other application window as first window.
    AddWindow({Application::kFoobar, Mode::kEditor, 0, /*on_screen=*/true});
    SCWindow* editor_window = AddWindow(
        {Application::kPowerPoint, Mode::kEditor, 0, /*on_screen=*/true});
    // Add some more applications.
    AddWindow({Application::kKeynote, Mode::kEditor, 0, /*on_screen=*/true});
    AddWindow({Application::kOpenOffice, Mode::kEditor, 0, /*on_screen=*/true});

    std::unique_ptr<ScreenCaptureKitFullscreenModule> fullscreen_module =
        MaybeCreateScreenCaptureKitFullscreenModule(
            base::SingleThreadTaskRunner::GetCurrentDefault(),
            reset_stream_interface, editor_window);
    EXPECT_TRUE(fullscreen_module);
    EXPECT_EQ(fullscreen_module->get_mode(),
              ScreenCaptureKitFullscreenModule::Mode::kPowerPoint);

    // Unretained since the test fixture must outlive the test.
    fullscreen_module->set_get_sharable_content_for_test(
        base::BindRepeating(&SCKFullscreenModuleTest::getShareableContentMock,
                            base::Unretained(this)));
    // Start process of checking for new fullscreen windows and run for a few
    // seconds.
    fullscreen_module->Start();
    StepForward(base::Seconds(1), /*steps=*/4);
    EXPECT_FALSE(fullscreen_module->is_fullscreen_window_active());

    // Change to fullscreen mode.
    SCWindow* slideshow_window = AddWindow(
        {Application::kPowerPoint, Mode::kSlideshow, 0, /*on_screen=*/true});
    AddWindow({Application::kPowerPoint, Mode::kPresentersView, 0,
               /*on_screen=*/true});
    EXPECT_CALL(reset_stream_interface,
                ResetStreamTo(MatchesScWindow(slideshow_window.windowID)))
        .Times(1);
    StepForward(base::Seconds(1), /*steps=*/1);
    EXPECT_TRUE(fullscreen_module->is_fullscreen_window_active());
    StepForward(base::Seconds(1), /*steps=*/4);
    EXPECT_TRUE(fullscreen_module->is_fullscreen_window_active());
    // For PowerPoint, the fullscreen window is closed once the slideshow stops.
    // The stream will then be reset to the editor window by
    // ScreenCaptureKitDeviceMac.
  }
}

TEST_F(SCKFullscreenModuleTest, DetectFullscreenWindowKeynote) {
  if (@available(macOS 13.1, *)) {
    MockResetStreamInterface reset_stream_interface;

    // Add other application window as first window.
    AddWindow({Application::kFoobar, Mode::kEditor, 0, /*on_screen=*/true});
    SCWindow* editor_window = AddWindow(
        {Application::kKeynote, Mode::kEditor, 0, /*on_screen=*/true});
    // Add some more applications.
    AddWindow({Application::kPowerPoint, Mode::kEditor, 0, /*on_screen=*/true});
    AddWindow({Application::kOpenOffice, Mode::kEditor, 0, /*on_screen=*/true});

    std::unique_ptr<ScreenCaptureKitFullscreenModule> fullscreen_module =
        MaybeCreateScreenCaptureKitFullscreenModule(
            base::SingleThreadTaskRunner::GetCurrentDefault(),
            reset_stream_interface, editor_window);
    EXPECT_TRUE(fullscreen_module);
    EXPECT_EQ(fullscreen_module->get_mode(),
              ScreenCaptureKitFullscreenModule::Mode::kKeynote);

    // Unretained since the test fixture must outlive the test.
    fullscreen_module->set_get_sharable_content_for_test(
        base::BindRepeating(&SCKFullscreenModuleTest::getShareableContentMock,
                            base::Unretained(this)));
    // Start process of checking for new fullscreen windows and run for a few
    // seconds.
    fullscreen_module->Start();
    StepForward(base::Seconds(1), /*steps=*/4);
    EXPECT_FALSE(fullscreen_module->is_fullscreen_window_active());

    // Hide editor and change to fullscreen mode.
    SetWindowOnScreen(editor_window.windowID, false);
    SCWindow* slideshow_window = AddWindow(
        {Application::kKeynote, Mode::kSlideshow, 0, /*on_screen=*/true});
    SCWindow* presenters_view_window =
        AddWindow({Application::kKeynote, Mode::kPresentersView, 0,
                   /*on_screen=*/true});
    EXPECT_CALL(reset_stream_interface,
                ResetStreamTo(MatchesScWindow(slideshow_window.windowID)))
        .Times(1);

    StepForward(base::Seconds(1), /*steps=*/1);
    EXPECT_TRUE(fullscreen_module->is_fullscreen_window_active());
    StepForward(base::Seconds(1), /*steps=*/4);
    EXPECT_TRUE(fullscreen_module->is_fullscreen_window_active());

    // Hide fullscreen windows and restore editor window.
    SetWindowOnScreen(editor_window.windowID, true);
    SetWindowOnScreen(slideshow_window.windowID, false);
    SetWindowOnScreen(presenters_view_window.windowID, false);

    EXPECT_CALL(reset_stream_interface,
                ResetStreamTo(MatchesScWindow(editor_window.windowID)))
        .Times(1);
    task_environment_.FastForwardBy(base::Seconds(1));
    EXPECT_FALSE(fullscreen_module->is_fullscreen_window_active());
  }
}

TEST_F(SCKFullscreenModuleTest, DetectFullscreenWindowOpenOfficeImpress) {
  if (@available(macOS 13.1, *)) {
    MockResetStreamInterface reset_stream_interface;

    // Add other application window as first window.
    AddWindow({Application::kFoobar, Mode::kEditor, 0, /*on_screen=*/true});
    SCWindow* editor_window = AddWindow(
        {Application::kOpenOffice, Mode::kEditor, 0, /*on_screen=*/true});
    SCWindow* second_editor_window = AddWindow(
        {Application::kOpenOffice, Mode::kEditor, 1, /*on_screen=*/true});
    // Add some more applications.
    AddWindow({Application::kPowerPoint, Mode::kEditor, 0, /*on_screen=*/true});
    AddWindow({Application::kKeynote, Mode::kEditor, 0, /*on_screen=*/true});

    std::unique_ptr<ScreenCaptureKitFullscreenModule> fullscreen_module =
        MaybeCreateScreenCaptureKitFullscreenModule(
            base::SingleThreadTaskRunner::GetCurrentDefault(),
            reset_stream_interface, editor_window);
    EXPECT_TRUE(fullscreen_module);
    EXPECT_EQ(fullscreen_module->get_mode(),
              ScreenCaptureKitFullscreenModule::Mode::kOpenOffice);

    // Unretained since the test fixture must outlive the test.
    fullscreen_module->set_get_sharable_content_for_test(
        base::BindRepeating(&SCKFullscreenModuleTest::getShareableContentMock,
                            base::Unretained(this)));
    // Start process of checking for new fullscreen windows and run for a few
    // seconds.
    fullscreen_module->Start();
    StepForward(base::Seconds(1), /*steps=*/4);
    EXPECT_FALSE(fullscreen_module->is_fullscreen_window_active());

    // Change to fullscreen mode.
    SCWindow* slideshow_window = AddWindow(
        {Application::kOpenOffice, Mode::kSlideshow, 0, /*on_screen=*/true});
    SCWindow* presenters_view_window =
        AddWindow({Application::kOpenOffice, Mode::kPresentersView, 0,
                   /*on_screen=*/true});
    EXPECT_CALL(reset_stream_interface,
                ResetStreamTo(MatchesScWindow(slideshow_window.windowID)))
        .Times(1);

    // Ignore fullscreen window since there are two presentations open and we
    // cannot determine which presentation is in full screen.
    StepForward(base::Seconds(1), /*steps=*/4);
    EXPECT_FALSE(fullscreen_module->is_fullscreen_window_active());

    // Close the second document window and verify that the fullscreen window is
    // detected.
    SetWindowOnScreen(second_editor_window.windowID, false);
    StepForward(base::Seconds(1), /*steps=*/1);
    EXPECT_TRUE(fullscreen_module->is_fullscreen_window_active());
    StepForward(base::Seconds(1), /*steps=*/4);
    EXPECT_TRUE(fullscreen_module->is_fullscreen_window_active());

    // Hide fullscreen windows and restore editor window.
    SetWindowOnScreen(editor_window.windowID, true);
    SetWindowOnScreen(slideshow_window.windowID, false);
    SetWindowOnScreen(presenters_view_window.windowID, false);

    EXPECT_CALL(reset_stream_interface,
                ResetStreamTo(MatchesScWindow(editor_window.windowID)))
        .Times(1);
    task_environment_.FastForwardBy(base::Seconds(1));
    EXPECT_FALSE(fullscreen_module->is_fullscreen_window_active());
  }
}

}  // namespace content