chromium/components/exo/wayland/wayland_aura_shell_server_test.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 <wayland-server-protocol-core.h>

#include "ash/shell.h"
#include "ash/test/test_widget_builder.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_test_util.h"
#include "base/memory/raw_ptr.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/exo/display.h"
#include "components/exo/wayland/test/client_util.h"
#include "components/exo/wayland/test/server_util.h"
#include "components/exo/wayland/test/wayland_server_test.h"
#include "components/exo/wayland/xdg_shell.h"
#include "components/exo/xdg_shell_surface.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"

namespace exo::wayland {
namespace {

class ClientData : public test::TestClient::CustomData {
 public:
  struct TestShellSurface {
    std::unique_ptr<wl_surface> surface;
    std::unique_ptr<wl_shell_surface> shell_surface;
    std::unique_ptr<xdg_toplevel> xdg_toplevel;
    std::unique_ptr<xdg_surface> xdg_surface;
  };
  std::vector<TestShellSurface> test_surfaces_list;
};

class WaylandAuraShellServerTest : public test::WaylandServerTest {
 public:
  struct TestSurfaceKey {
    test::ResourceKey surface_key;
    test::ResourceKey shell_surface_key;
  };

  WaylandAuraShellServerTest() = default;

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

  ~WaylandAuraShellServerTest() override = default;

  // test::WaylandServerTest:
  void SetUp() override {
    WaylandServerTest::SetUp();
    display_ = server_->GetDisplay();
  }

  void TearDown() override { WaylandServerTest::TearDown(); }

  std::vector<TestSurfaceKey> SetupClientSurfaces(
      int num_test_surfaces_list = 1) {
    std::vector<TestSurfaceKey> keys;

    PostToClientAndWait([&](test::TestClient* client) {
      auto data = std::make_unique<ClientData>();
      for (int i = 0; i < num_test_surfaces_list; ++i) {
        ClientData::TestShellSurface test_surface;
        test_surface.surface.reset(
            wl_compositor_create_surface(client->compositor()));

        test_surface.xdg_surface.reset(xdg_wm_base_get_xdg_surface(
            client->globals().xdg_wm_base.get(), test_surface.surface.get()));
        test_surface.xdg_toplevel.reset(
            xdg_surface_get_toplevel(test_surface.xdg_surface.get()));

        keys.push_back(TestSurfaceKey{
            .surface_key =
                test::client_util::GetResourceKey(test_surface.surface.get()),
            .shell_surface_key = test::client_util::GetResourceKey(
                test_surface.xdg_surface.get()),
        });
        data->test_surfaces_list.push_back(std::move(test_surface));
      }
      client->set_data(std::move(data));
    });

    return keys;
  }

  void AttachBufferToSurfaces() {
    PostToClientAndWait([&](test::TestClient* client) {
      ASSERT_TRUE(client->InitShmBufferFactory(256 * 256 * 4));

      auto* data = client->GetDataAs<ClientData>();
      for (auto& test_surface : data->test_surfaces_list) {
        auto buffer = client->shm_buffer_factory()->CreateBuffer(0, 64, 64);
        wl_surface_attach(test_surface.surface.get(), buffer->resource(), 0, 0);
        wl_surface_commit(test_surface.surface.get());
      }
    });
  }

  struct ShellObserver {
    // For focus.
    raw_ptr<wl_surface> gained_active;
    raw_ptr<wl_surface> lost_active;
    int32_t activated_call_count = 0;

    // For overview.
    int32_t overview_entered_call_count = 0;
    int32_t overview_exited_call_count = 0;
  };

  const zaura_shell_listener kAuraShellListener = {
      [](void* data, struct zaura_shell* zaura_shell, uint32_t layout_mode) {},
      [](void* data, struct zaura_shell* zaura_shell, uint32_t id) {},
      [](void* data,
         struct zaura_shell* zaura_shell,
         struct wl_array* desk_names) {},
      [](void* data,
         struct zaura_shell* zaura_shell,
         int32_t active_desk_index) {},
      [](void* data,
         struct zaura_shell* zaura_shell,
         struct wl_surface* gained_active,
         struct wl_surface* lost_active) {
        auto* observer = static_cast<ShellObserver*>(data);
        observer->gained_active = gained_active;
        observer->lost_active = lost_active;
        observer->activated_call_count++;
      },
      [](void* data, struct zaura_shell* zaura_shell) {
        auto* observer = static_cast<ShellObserver*>(data);
        observer->overview_entered_call_count++;
      },
      [](void* data, struct zaura_shell* zaura_shell) {
        auto* observer = static_cast<ShellObserver*>(data);
        observer->overview_exited_call_count++;
      }};

  std::unique_ptr<ShellObserver> SetupShellObservation() {
    auto observer = std::make_unique<ShellObserver>();
    PostToClientAndWait([&](test::TestClient* client) {
      zaura_shell_add_listener(client->aura_shell(), &kAuraShellListener,
                               observer.get());
    });
    return observer;
  }

  Surface* GetClientSurface(test::ResourceKey surface_key) {
    return test::server_util::GetUserDataForResource<Surface>(server_.get(),
                                                              surface_key);
  }

  raw_ptr<Display, DanglingUntriaged> display_;
};

// Home screen -> any window
TEST_F(WaylandAuraShellServerTest, HasFocusedClientChangedSendActivated) {
  auto keys = SetupClientSurfaces();
  auto observer = SetupShellObservation();

  Surface* surface = GetClientSurface(keys[0].surface_key);
  ASSERT_TRUE(surface);

  display_->seat()->OnWindowFocused(surface->window(), nullptr);
  // Wait until all wayland events are sent.
  PostToClientAndWait([]() {});
  EXPECT_TRUE(observer->gained_active != nullptr);
  EXPECT_TRUE(observer->lost_active == nullptr);
  EXPECT_EQ(1, observer->activated_call_count);
}

// Exo client window -> Same exo client another window
TEST_F(WaylandAuraShellServerTest, FocusedClientChangedSendActivated) {
  auto keys = SetupClientSurfaces(2);
  auto observer = SetupShellObservation();

  Surface* surface = GetClientSurface(keys[0].surface_key);
  ASSERT_TRUE(surface);

  display_->seat()->OnWindowFocused(surface->window(), nullptr);
  // Reset previous gained and lost active info.
  observer->gained_active = nullptr;
  observer->lost_active = nullptr;

  Surface* surface2 = GetClientSurface(keys[1].surface_key);
  ASSERT_TRUE(surface2);
  display_->seat()->OnWindowFocused(surface2->window(), surface->window());
  // Wait until all wayland events are sent.
  PostToClientAndWait([]() {});

  EXPECT_TRUE(observer->gained_active != nullptr);
  EXPECT_TRUE(observer->lost_active != nullptr);
  EXPECT_EQ(2, observer->activated_call_count);
}

// Exo client window -> Chrome window
TEST_F(WaylandAuraShellServerTest, FocusedClientChangedToNonExoSendActivated) {
  auto keys = SetupClientSurfaces(2);
  auto observer = SetupShellObservation();

  Surface* surface = GetClientSurface(keys[0].surface_key);
  ASSERT_TRUE(surface);
  display_->seat()->OnWindowFocused(surface->window(), nullptr);

  // Reset previous gained and lost active info.
  observer->gained_active = nullptr;
  observer->lost_active = nullptr;

  Surface* surface2 = GetClientSurface(keys[1].surface_key);
  ASSERT_TRUE(surface2);
  // Chrome surface doesn't have wayland resource.
  SetSurfaceResource(surface2, nullptr);
  display_->seat()->OnWindowFocused(surface2->window(), surface->window());
  // Wait until all wayland events are sent.
  PostToClientAndWait([]() {});

  EXPECT_TRUE(observer->gained_active == nullptr);
  EXPECT_TRUE(observer->lost_active != nullptr);
  EXPECT_EQ(2, observer->activated_call_count);
}

// Chrome window -> Chrome window
TEST_F(WaylandAuraShellServerTest,
       NonExoFocusedClientChangedNotSendingActivated) {
  auto keys = SetupClientSurfaces(2);
  auto observer = SetupShellObservation();

  Surface* surface = GetClientSurface(keys[0].surface_key);
  ASSERT_TRUE(surface);

  // Chrome surface doesn't have wayland resource.
  SetSurfaceResource(surface, nullptr);
  display_->seat()->OnWindowFocused(surface->window(), nullptr);

  // Reset previous gained and lost active info.
  observer->gained_active = nullptr;
  observer->lost_active = nullptr;

  Surface* surface2 = GetClientSurface(keys[1].surface_key);
  ASSERT_TRUE(surface2);
  // Chrome surface doesn't have wayland resource.
  SetSurfaceResource(surface2, nullptr);
  display_->seat()->OnWindowFocused(surface2->window(), surface->window());
  // Wait until all wayland events are sent.
  PostToClientAndWait([]() {});

  EXPECT_EQ(nullptr, observer->gained_active.get());
  EXPECT_EQ(nullptr, observer->lost_active.get());
  EXPECT_EQ(1, observer->activated_call_count);
}

TEST_F(WaylandAuraShellServerTest, RotateFocus) {
  auto keys = SetupClientSurfaces();
  AttachBufferToSurfaces();

  struct RotateFocusListener {
    uint32_t last_received_serial;
    uint32_t last_received_direction;
    uint32_t last_received_restart;
  };
  RotateFocusListener listener;

  zaura_toplevel_listener listeners = {
      [](void*, zaura_toplevel*, int32_t, int32_t, int32_t, int32_t,
         wl_array*) {},
      [](void*, zaura_toplevel*, int32_t, int32_t) {},
      [](void*, zaura_toplevel*, uint32_t) {},
      [](void* data, zaura_toplevel*, uint32_t serial, uint32_t direction,
         uint32_t restart) {
        auto* listener = static_cast<RotateFocusListener*>(data);
        listener->last_received_serial = serial;
        listener->last_received_direction = direction;
        listener->last_received_restart = restart;
      },
  };

  std::unique_ptr<zaura_toplevel> zaura_toplevel;

  PostToClientAndWait([&](test::TestClient* client) {
    auto* data = client->GetDataAs<ClientData>();

    zaura_toplevel.reset(zaura_shell_get_aura_toplevel_for_xdg_toplevel(
        client->globals().aura_shell.get(),
        data->test_surfaces_list[0].xdg_toplevel.get()));
    zaura_toplevel_add_listener(zaura_toplevel.get(), &listeners, &listener);
    zaura_toplevel_set_supports_screen_coordinates(zaura_toplevel.get());
  });

  auto* xdg_surface =
      test::server_util::GetUserDataForResource<WaylandXdgSurface>(
          server_.get(), keys[0].shell_surface_key);
  ASSERT_TRUE(xdg_surface);
  XdgShellSurface* shell_surface = xdg_surface->shell_surface.get();
  ASSERT_TRUE(shell_surface);

  PostToClientAndWait([]() {});

  // Serial should be increasing.
  uint32_t received_serial = 0;

  shell_surface->RotatePaneFocusFromView(nullptr, true, true);
  PostToClientAndWait([]() {});
  EXPECT_EQ(ZAURA_TOPLEVEL_ROTATE_DIRECTION_FORWARD,
            listener.last_received_direction);
  EXPECT_EQ(ZAURA_TOPLEVEL_ROTATE_RESTART_STATE_RESTART,
            listener.last_received_restart);
  // No assertion for serial on the first run. We just need to ensure that it
  // increases next time.
  received_serial = listener.last_received_serial;

  shell_surface->RotatePaneFocusFromView(nullptr, false, true);
  PostToClientAndWait([]() {});
  EXPECT_GT(listener.last_received_serial, received_serial);
  EXPECT_EQ(ZAURA_TOPLEVEL_ROTATE_DIRECTION_BACKWARD,
            listener.last_received_direction);
  EXPECT_EQ(ZAURA_TOPLEVEL_ROTATE_RESTART_STATE_RESTART,
            listener.last_received_restart);
  received_serial = listener.last_received_serial;

  shell_surface->RotatePaneFocusFromView(nullptr, true, false);
  PostToClientAndWait([]() {});
  EXPECT_GT(listener.last_received_serial, received_serial);
  EXPECT_EQ(ZAURA_TOPLEVEL_ROTATE_DIRECTION_FORWARD,
            listener.last_received_direction);
  EXPECT_EQ(ZAURA_TOPLEVEL_ROTATE_RESTART_STATE_NO_RESTART,
            listener.last_received_restart);
  received_serial = listener.last_received_serial;
}

TEST_F(WaylandAuraShellServerTest, AckRotateFocus) {
  auto keys = SetupClientSurfaces();
  AttachBufferToSurfaces();

  auto native_widget1 = ash::TestWidgetBuilder().BuildOwnsNativeWidget();
  auto native_widget2 = ash::TestWidgetBuilder().BuildOwnsNativeWidget();

  std::unique_ptr<zaura_toplevel> zaura_toplevel;

  PostToClientAndWait([&](test::TestClient* client) {
    auto* data = client->GetDataAs<ClientData>();
    zaura_toplevel.reset(zaura_shell_get_aura_toplevel_for_xdg_toplevel(
        client->globals().aura_shell.get(),
        data->test_surfaces_list[0].xdg_toplevel.get()));
    zaura_toplevel_set_supports_screen_coordinates(zaura_toplevel.get());
  });

  uint32_t serial = 0;

  WaylandXdgSurface* xdg_surface =
      test::server_util::GetUserDataForResource<WaylandXdgSurface>(
          server_.get(), keys[0].shell_surface_key);
  ASSERT_TRUE(xdg_surface);
  xdg_surface->shell_surface->set_rotate_focus_callback(
      base::BindLambdaForTesting(
          [&serial](ash::FocusCycler::Direction, bool) { return serial; }));

  auto* focus_cycler = ash::Shell::Get()->focus_cycler();
  focus_cycler->AddWidget(native_widget1.get());
  focus_cycler->AddWidget(xdg_surface->shell_surface->GetWidget());
  focus_cycler->AddWidget(native_widget2.get());

  focus_cycler->FocusWidget(xdg_surface->shell_surface->GetWidget());
  ASSERT_TRUE(xdg_surface->shell_surface->GetWidget()->IsActive());

  // Handled should result in no change.
  xdg_surface->shell_surface->RotatePaneFocusFromView(nullptr, true, true);
  PostToClientAndWait([&](test::TestClient* client) {
    zaura_toplevel_ack_rotate_focus(
        zaura_toplevel.get(), serial++,
        ZAURA_TOPLEVEL_ROTATE_HANDLED_STATE_HANDLED);
  });
  EXPECT_TRUE(xdg_surface->shell_surface->GetWidget()->IsActive());

  // Unhandled should result in a rotation forward.
  xdg_surface->shell_surface->RotatePaneFocusFromView(nullptr, true, true);
  PostToClientAndWait([&](test::TestClient* client) {
    zaura_toplevel_ack_rotate_focus(
        zaura_toplevel.get(), serial++,
        ZAURA_TOPLEVEL_ROTATE_HANDLED_STATE_NOT_HANDLED);
  });
  EXPECT_TRUE(native_widget2->IsActive());

  // Reset
  focus_cycler->FocusWidget(xdg_surface->shell_surface->GetWidget());
  ASSERT_TRUE(xdg_surface->shell_surface->GetWidget()->IsActive());

  // Unhandled should result in a rotation backward.
  xdg_surface->shell_surface->RotatePaneFocusFromView(nullptr, false, true);
  PostToClientAndWait([&](test::TestClient* client) {
    zaura_toplevel_ack_rotate_focus(
        zaura_toplevel.get(), serial++,
        ZAURA_TOPLEVEL_ROTATE_HANDLED_STATE_NOT_HANDLED);
  });
  EXPECT_TRUE(native_widget1->IsActive());
}

TEST_F(WaylandAuraShellServerTest, OverviewMode) {
  auto* overview_controller = ash::Shell::Get()->overview_controller();
  const auto start_action = ash::OverviewStartAction::kTests;
  const auto end_action = ash::OverviewEndAction::kTests;

  auto observer = SetupShellObservation();

  // Need at least one window for overview animation.
  auto native_widget = ash::TestWidgetBuilder().BuildOwnsNativeWidget();

  ui::ScopedAnimationDurationScaleMode non_zero(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Test starting overview and letting the animation complete.
  overview_controller->StartOverview(start_action);
  ash::WaitForOverviewEnterAnimation();

  // Wait until all wayland events are sent.
  PostToClientAndWait([]() {});
  EXPECT_EQ(1, observer->overview_entered_call_count);
  EXPECT_EQ(0, observer->overview_exited_call_count);

  // Test ending overview and letting the animation complete.
  overview_controller->EndOverview(end_action);
  ash::WaitForOverviewExitAnimation();
  PostToClientAndWait([]() {});
  EXPECT_EQ(1, observer->overview_entered_call_count);
  EXPECT_EQ(1, observer->overview_exited_call_count);

  // Test starting overview but ending overview before the animation completes.
  // We don't send a start signal.
  overview_controller->StartOverview(start_action);
  overview_controller->EndOverview(end_action);
  ash::WaitForOverviewExitAnimation();
  PostToClientAndWait([]() {});
  EXPECT_EQ(1, observer->overview_entered_call_count);
  EXPECT_EQ(2, observer->overview_exited_call_count);

  // Enter overview to prepare for the next test.
  overview_controller->StartOverview(start_action);
  ash::WaitForOverviewEnterAnimation();
  PostToClientAndWait([]() {});
  EXPECT_EQ(2, observer->overview_entered_call_count);
  EXPECT_EQ(2, observer->overview_exited_call_count);

  // Test ending overview but ending overview before the animation completes.
  // We don't send an end signal.
  overview_controller->EndOverview(end_action);
  overview_controller->StartOverview(start_action);
  ash::WaitForOverviewEnterAnimation();
  PostToClientAndWait([]() {});
  EXPECT_EQ(3, observer->overview_entered_call_count);
  EXPECT_EQ(2, observer->overview_exited_call_count);
}

TEST_F(WaylandAuraShellServerTest, SetCanMaximizeAndFullscreen) {
  auto keys = SetupClientSurfaces();
  AttachBufferToSurfaces();

  std::unique_ptr<zaura_toplevel> zaura_toplevel;
  PostToClientAndWait([&](test::TestClient* client) {
    auto* data = client->GetDataAs<ClientData>();
    zaura_toplevel.reset(zaura_shell_get_aura_toplevel_for_xdg_toplevel(
        client->globals().aura_shell.get(),
        data->test_surfaces_list[0].xdg_toplevel.get()));
  });

  WaylandXdgSurface* xdg_surface =
      test::server_util::GetUserDataForResource<WaylandXdgSurface>(
          server_.get(), keys[0].shell_surface_key);
  ASSERT_TRUE(xdg_surface);

  auto* widget = xdg_surface->shell_surface->GetWidget();

  PostToClientAndWait([&](test::TestClient* client) {
    zaura_toplevel_set_can_maximize(zaura_toplevel.get());
  });
  EXPECT_TRUE(widget->widget_delegate()->CanMaximize());

  PostToClientAndWait([&](test::TestClient* client) {
    zaura_toplevel_unset_can_maximize(zaura_toplevel.get());
  });
  EXPECT_FALSE(widget->widget_delegate()->CanMaximize());

  PostToClientAndWait([&](test::TestClient* client) {
    zaura_toplevel_set_can_fullscreen(zaura_toplevel.get());
  });
  EXPECT_TRUE(widget->widget_delegate()->CanFullscreen());

  PostToClientAndWait([&](test::TestClient* client) {
    zaura_toplevel_unset_can_fullscreen(zaura_toplevel.get());
  });
  EXPECT_FALSE(widget->widget_delegate()->CanFullscreen());
}

// TODO(crbug.com/40284737): Re-enable this when flakiness is resolved.
TEST_F(WaylandAuraShellServerTest, DISABLED_SetUnSetFloat) {
  UpdateDisplay("800x600");

  auto keys = SetupClientSurfaces();
  AttachBufferToSurfaces();

  std::unique_ptr<zaura_toplevel> zaura_toplevel;
  PostToClientAndWait([&](test::TestClient* client) {
    auto* data = client->GetDataAs<ClientData>();
    zaura_toplevel.reset(zaura_shell_get_aura_toplevel_for_xdg_toplevel(
        client->globals().aura_shell.get(),
        data->test_surfaces_list[0].xdg_toplevel.get()));
  });

  WaylandXdgSurface* xdg_surface =
      test::server_util::GetUserDataForResource<WaylandXdgSurface>(
          server_.get(), keys[0].shell_surface_key);
  ASSERT_TRUE(xdg_surface);

  views::Widget* widget = xdg_surface->shell_surface->GetWidget();
  auto* window_state = ash::WindowState::Get(widget->GetNativeWindow());
  window_state->window()->SetProperty(chromeos::kAppTypeKey,
                                      chromeos::AppType::LACROS);
  ASSERT_FALSE(window_state->IsFloated());

  // Location 0 is bottom right. Test that the window is floated and in the
  // bottom right quadrant of the display.
  PostToClientAndWait([&](test::TestClient* client) {
    zaura_toplevel_set_float_to_location(zaura_toplevel.get(), /*location=*/0u);
  });
  EXPECT_TRUE(window_state->IsFloated());
  EXPECT_TRUE(gfx::Rect(400, 300, 400, 300)
                  .Contains(widget->GetWindowBoundsInScreen()));

  // Unfloat the window.
  PostToClientAndWait([&](test::TestClient* client) {
    zaura_toplevel_unset_float(zaura_toplevel.get());
  });
  EXPECT_FALSE(window_state->IsFloated());

  // Location 1 is bottom left. Test that the window is floated and in the
  // bottom left quadrant of the display.
  PostToClientAndWait([&](test::TestClient* client) {
    zaura_toplevel_set_float_to_location(zaura_toplevel.get(), /*location=*/1u);
  });
  EXPECT_TRUE(window_state->IsFloated());
  EXPECT_TRUE(
      gfx::Rect(0, 300, 400, 300).Contains(widget->GetWindowBoundsInScreen()));
}

class WaylandAuraOutputServerTest : public test::WaylandServerTest {
 public:
  struct AuraOutputObserver {
    void Reset() { is_active = false; }
    bool is_active = false;
  };

  WaylandAuraOutputServerTest() = default;
  WaylandAuraOutputServerTest(const WaylandAuraOutputServerTest&) = delete;
  WaylandAuraOutputServerTest& operator=(const WaylandAuraOutputServerTest&) =
      delete;
  ~WaylandAuraOutputServerTest() override = default;

  std::unique_ptr<AuraOutputObserver> SetupAuraOutput(wl_output* output) {
    static constexpr zaura_output_listener kAuraOutputListener = {
        [](void*, struct zaura_output*, uint32_t, uint32_t) {},
        [](void*, struct zaura_output*, uint32_t) {},
        [](void*, struct zaura_output*, uint32_t) {},
        [](void*, struct zaura_output*, int32_t, int32_t, int32_t, int32_t) {},
        [](void*, struct zaura_output*, int32_t) {},
        [](void*, struct zaura_output*, uint32_t, uint32_t) {},
        [](void* data, struct zaura_output* zaura_output) {
          auto* observer = static_cast<AuraOutputObserver*>(data);
          observer->is_active = true;
        }};
    auto observer = std::make_unique<AuraOutputObserver>();
    PostToClientAndWait([&](test::TestClient* client) {
      std::unique_ptr<zaura_output> aura_output(
          zaura_shell_get_aura_output(client->aura_shell(), output));
      zaura_output_add_listener(aura_output.get(), &kAuraOutputListener,
                                observer.get());
      client->globals().aura_outputs.emplace_back(std::move(aura_output));
    });
    return observer;
  }
};

TEST_F(WaylandAuraOutputServerTest, ActiveDisplay) {
  UpdateDisplay("800x600,800x600");
  const auto* screen = display::Screen::GetScreen();
  ASSERT_EQ(2u, screen->GetAllDisplays().size());
  const int64_t primary_id = screen->GetAllDisplays()[0].id();
  const int64_t secondary_id = screen->GetAllDisplays()[1].id();

  wl_output* primary_output = nullptr;
  wl_output* secondary_output = nullptr;
  PostToClientAndWait([&](test::TestClient* client) {
    primary_output = client->globals().outputs[0].get();
    secondary_output = client->globals().outputs[1].get();
  });

  // Create two widgets, one on the primary and the other on the secondary
  // display.
  auto* primary_widget = ash::TestWidgetBuilder()
                             .SetBounds({{100, 100}, {200, 200}})
                             .BuildOwnedByNativeWidget();
  auto* secondary_widget = ash::TestWidgetBuilder()
                               .SetBounds({{900, 100}, {200, 200}})
                               .BuildOwnedByNativeWidget();
  ASSERT_EQ(
      screen->GetDisplayNearestWindow(primary_widget->GetNativeWindow()).id(),
      primary_id);
  ASSERT_EQ(
      screen->GetDisplayNearestWindow(secondary_widget->GetNativeWindow()).id(),
      secondary_id);

  // Initialize the aura output extensions and register observers.
  auto primary_observer = SetupAuraOutput(primary_output);
  auto secondary_observer = SetupAuraOutput(secondary_output);
  EXPECT_FALSE(primary_observer->is_active);
  EXPECT_FALSE(secondary_observer->is_active);

  // Activate the widget on the primary display.
  primary_widget->Activate();
  PostToClientAndWait([]() {});
  EXPECT_TRUE(primary_observer->is_active);
  EXPECT_FALSE(secondary_observer->is_active);

  primary_observer->Reset();
  secondary_observer->Reset();

  // Activate the widget on the secondary display.
  secondary_widget->Activate();
  PostToClientAndWait([]() {});
  EXPECT_FALSE(primary_observer->is_active);
  EXPECT_TRUE(secondary_observer->is_active);

  primary_observer->Reset();
  secondary_observer->Reset();

  // Ensure activating the widget on the primary display again correctly
  // re-emits the activate event for the primary output.
  primary_widget->Activate();
  PostToClientAndWait([]() {});
  EXPECT_TRUE(primary_observer->is_active);
  EXPECT_FALSE(secondary_observer->is_active);
}

}  // namespace
}  // namespace exo::wayland