blob: 5db9f652c79ce35738551f0f65435f6efcd2f72f [file] [log] [blame]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "mock-viewporter-shim.h" // NOLINT(build/include_directory)
#include "testing/x11-test-base.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <wayland-client-protocol.h>
#include <wayland-client.h>
#include <wayland-server-core.h>
#include <wayland-server-protocol.h>
#include <wayland-util.h>
#include <xcb/xcb.h>
#include <xcb/xproto.h>
#include <cstdint>
#ifdef QUIRKS_SUPPORT
#include "quirks/sommelier-quirks.h"
#endif
namespace vm_tools {
namespace sommelier {
using ::testing::_;
using ::testing::AllOf;
using ::testing::PrintToString;
using X11Test = X11TestBase;
TEST_F(X11DirectScaleTest, ViewportOverrideStretchedVertically) {
// Arrange
ctx.viewport_resize = true;
AdvertiseOutputs(xwayland.get(),
{{.logical_width = 1536, .logical_height = 864}});
sl_window* window = CreateToplevelWindow();
const int pixel_width = 1920;
const int pixel_height = 1080;
window->managed = true;
window->max_width = pixel_width;
window->min_height = pixel_height;
window->min_width = pixel_width;
window->max_height = pixel_height;
window->width = pixel_width;
window->height = pixel_height;
window->paired_surface->contents_width = pixel_width;
window->paired_surface->contents_height = pixel_height;
wl_array states;
wl_array_init(&states);
// Assert
EXPECT_CALL(mock_viewport_shim_,
set_destination(window->paired_surface->viewport, 1393, 784))
.Times(1);
// Act: Configure with height smaller than min_height.
HostEventHandler(window->xdg_toplevel)
->configure(nullptr, window->xdg_toplevel, 1536 /*1920*/, 784 /*980*/,
&states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, 123);
sl_host_surface_commit(nullptr, window->paired_surface->resource);
Pump();
// Assert: viewport size and pointer scale are set, width height are
// unchanged.
EXPECT_TRUE(window->viewport_override);
EXPECT_EQ(window->viewport_width, 1393);
EXPECT_EQ(window->viewport_height, 784);
EXPECT_FLOAT_EQ(window->viewport_pointer_scale, 1.1026561);
EXPECT_EQ(window->width, 1920);
EXPECT_EQ(window->height, 1080);
// Assert
EXPECT_CALL(mock_viewport_shim_,
set_destination(window->paired_surface->viewport, -1, -1))
// Act: Configure with the size that is within the window bounds.
.Times(1);
HostEventHandler(window->xdg_toplevel)
->configure(nullptr, window->xdg_toplevel, 1536, 864, &states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, 124);
// Assert
EXPECT_CALL(mock_viewport_shim_,
set_destination(window->paired_surface->viewport, 1536, 864))
.Times(1);
// Act
sl_host_surface_commit(nullptr, window->paired_surface->resource);
Pump();
// Assert: viewport override is no longer used.
EXPECT_FALSE(window->viewport_override);
EXPECT_EQ(window->viewport_width, -1);
EXPECT_EQ(window->viewport_height, -1);
EXPECT_EQ(window->width, pixel_width);
EXPECT_EQ(window->height, pixel_height);
}
TEST_F(X11DirectScaleTest, ViewportOverrideStretchedHorizontally) {
// Arrange
ctx.viewport_resize = true;
AdvertiseOutputs(xwayland.get(),
{{.logical_width = 1536, .logical_height = 864}});
sl_window* window = CreateToplevelWindow();
window->managed = true;
window->max_width = 1700;
window->max_height = 900;
window->min_width = 1600;
window->min_height = 800;
window->width = 1650;
window->height = 800;
window->paired_surface->contents_width = 1650;
window->paired_surface->contents_height = 800;
wl_array states;
wl_array_init(&states);
// Assert
EXPECT_CALL(mock_viewport_shim_,
set_destination(window->paired_surface->viewport, 1200, 581))
.Times(1);
// Act: Height is set to equal to max height, width smaller than min
// width.
HostEventHandler(window->xdg_toplevel)
->configure(nullptr, window->xdg_toplevel, 1200 /*1500*/, 720 /*900*/,
&states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, 123);
sl_host_surface_commit(nullptr, window->paired_surface->resource);
Pump();
// Assert: viewport size and pointer scale are set, width height are
// unchanged.
EXPECT_TRUE(window->viewport_override);
EXPECT_EQ(window->viewport_width, 1200);
EXPECT_EQ(window->viewport_height, 581);
EXPECT_FLOAT_EQ(window->viewport_pointer_scale, 1.1);
EXPECT_EQ(window->width, 1650);
EXPECT_EQ(window->height, 800);
// Assert
EXPECT_CALL(mock_viewport_shim_,
set_destination(window->paired_surface->viewport, -1, -1))
.Times(1);
// Act: Configure with the size that is within the window bounds.
HostEventHandler(window->xdg_toplevel)
->configure(nullptr, window->xdg_toplevel, 1360 /*1700*/, 680 /*850*/,
&states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, 124);
window->paired_surface->contents_width = 1700;
window->paired_surface->contents_height = 850;
// Assert
EXPECT_CALL(mock_viewport_shim_,
set_destination(window->paired_surface->viewport, 1360, 680))
.Times(1);
// Act
sl_host_surface_commit(nullptr, window->paired_surface->resource);
Pump();
// Assert: viewport override is no longer used.
EXPECT_FALSE(window->viewport_override);
EXPECT_EQ(window->viewport_width, -1);
EXPECT_EQ(window->viewport_height, -1);
EXPECT_EQ(window->width, 1700);
EXPECT_EQ(window->height, 850);
}
TEST_F(X11DirectScaleTest, ViewportOverrideSameAspectRatio) {
// Arrange
ctx.viewport_resize = true;
AdvertiseOutputs(xwayland.get(),
{{.logical_width = 1536, .logical_height = 864}});
sl_window* window = CreateToplevelWindow();
window->managed = true;
const int pixel_width = 1920;
const int pixel_height = 1080;
window->max_width = pixel_width;
window->min_height = pixel_height;
window->min_width = pixel_width;
window->max_height = pixel_height;
window->paired_surface->contents_width = pixel_width;
window->paired_surface->contents_height = pixel_height;
window->width = pixel_width;
window->height = pixel_height;
wl_array states;
wl_array_init(&states);
// Assert
EXPECT_CALL(mock_viewport_shim_,
set_destination(window->paired_surface->viewport, 1280, 720))
.Times(1);
// Act: Height and width squished while maintaining aspect ratio.
HostEventHandler(window->xdg_toplevel)
->configure(nullptr, window->xdg_toplevel, 1280 /*1600*/, 720 /*900*/,
&states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, 123);
sl_host_surface_commit(nullptr, window->paired_surface->resource);
Pump();
// Assert: viewport size and pointer scale are set, width height are
// unchanged.
EXPECT_TRUE(window->viewport_override);
EXPECT_EQ(window->viewport_width, 1280);
EXPECT_EQ(window->viewport_height, 720);
EXPECT_FLOAT_EQ(window->viewport_pointer_scale, 1.2);
EXPECT_EQ(window->width, pixel_width);
EXPECT_EQ(window->height, pixel_height);
// Assert
EXPECT_CALL(mock_viewport_shim_,
set_destination(window->paired_surface->viewport, -1, -1))
.Times(1);
// Act: Configure with the size that is within the window bounds.
HostEventHandler(window->xdg_toplevel)
->configure(nullptr, window->xdg_toplevel, 1536, 864, &states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, 123);
// Assert
EXPECT_CALL(mock_viewport_shim_,
set_destination(window->paired_surface->viewport, 1536, 864))
.Times(1);
// Act
sl_host_surface_commit(nullptr, window->paired_surface->resource);
Pump();
// Assert: viewport override is no longer used.
EXPECT_FALSE(window->viewport_override);
EXPECT_EQ(window->viewport_width, -1);
EXPECT_EQ(window->viewport_height, -1);
EXPECT_EQ(window->width, 1920);
EXPECT_EQ(window->height, 1080);
}
TEST_F(X11Test, TogglesFullscreenOnWmStateFullscreen) {
// Arrange: Create an xdg_toplevel surface. Initially it's not fullscreen.
AdvertiseOutputs(xwayland.get(), {OutputConfig()});
sl_window* window = CreateToplevelWindow();
uint32_t xdg_toplevel_id = XdgToplevelId(window);
EXPECT_EQ(window->fullscreen, 0);
Pump(); // exclude pending messages from EXPECT_CALL()s below
// Act: Pretend the window is owned by an X11 client requesting fullscreen.
// Sommelier receives the XCB_CLIENT_MESSAGE request due to its role as the
// X11 window manager. For test purposes, we skip creating a real X11
// connection and just call directly into the relevant handler.
xcb_client_message_event_t event;
event.response_type = XCB_CLIENT_MESSAGE;
event.format = 32;
event.window = window->id;
event.type = ctx.atoms[ATOM_NET_WM_STATE].value;
event.data.data32[0] = NET_WM_STATE_ADD;
event.data.data32[1] = ctx.atoms[ATOM_NET_WM_STATE_FULLSCREEN].value;
event.data.data32[2] = 0;
event.data.data32[3] = 0;
event.data.data32[4] = 0;
sl_handle_client_message(&ctx, &event);
// Assert: Sommelier records the fullscreen state.
EXPECT_EQ(window->fullscreen, 1);
// Assert: Sommelier forwards the fullscreen request to Exo.
EXPECT_CALL(
mock_wayland_channel_,
send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_SET_FULLSCREEN)))
.RetiresOnSaturation();
Pump();
// Act: Pretend the fictitious X11 client requests non-fullscreen.
event.data.data32[0] = NET_WM_STATE_REMOVE;
sl_handle_client_message(&ctx, &event);
// Assert: Sommelier records the fullscreen state.
EXPECT_EQ(window->fullscreen, 0);
// Assert: Sommelier forwards the unfullscreen request to Exo.
EXPECT_CALL(
mock_wayland_channel_,
send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_UNSET_FULLSCREEN)))
.RetiresOnSaturation();
}
TEST_F(X11Test, TogglesMaximizeOnWmStateMaximize) {
// Arrange: Create an xdg_toplevel surface. Initially it's not maximized.
sl_window* window = CreateToplevelWindow();
uint32_t xdg_toplevel_id = XdgToplevelId(window);
EXPECT_EQ(window->maximized, 0);
Pump(); // exclude pending messages from EXPECT_CALL()s below
// Act: Pretend an X11 client owns the surface, and requests to maximize it.
xcb_client_message_event_t event;
event.response_type = XCB_CLIENT_MESSAGE;
event.format = 32;
event.window = window->id;
event.type = ctx.atoms[ATOM_NET_WM_STATE].value;
event.data.data32[0] = NET_WM_STATE_ADD;
event.data.data32[1] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_HORZ].value;
event.data.data32[2] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_VERT].value;
event.data.data32[3] = 0;
event.data.data32[4] = 0;
sl_handle_client_message(&ctx, &event);
// Assert: Sommelier records the maximized state + forwards to Exo.
EXPECT_EQ(window->maximized, 1);
EXPECT_CALL(
mock_wayland_channel_,
send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_SET_MAXIMIZED)))
.RetiresOnSaturation();
Pump();
// Act: Pretend the fictitious X11 client requests to unmaximize.
event.data.data32[0] = NET_WM_STATE_REMOVE;
sl_handle_client_message(&ctx, &event);
// Assert: Sommelier records the unmaximized state + forwards to Exo.
EXPECT_EQ(window->maximized, 0);
EXPECT_CALL(
mock_wayland_channel_,
send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_UNSET_MAXIMIZED)))
.RetiresOnSaturation();
Pump();
}
TEST_F(X11Test, CanEnterFullscreenIfAlreadyMaximized) {
// Arrange
sl_window* window = CreateToplevelWindow();
uint32_t xdg_toplevel_id = XdgToplevelId(window);
Pump(); // exclude pending messages from EXPECT_CALL()s below
// Act: Pretend an X11 client owns the surface, and requests to maximize it.
xcb_client_message_event_t event;
event.response_type = XCB_CLIENT_MESSAGE;
event.format = 32;
event.window = window->id;
event.type = ctx.atoms[ATOM_NET_WM_STATE].value;
event.data.data32[0] = NET_WM_STATE_ADD;
event.data.data32[1] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_HORZ].value;
event.data.data32[2] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_VERT].value;
event.data.data32[3] = 0;
event.data.data32[4] = 0;
sl_handle_client_message(&ctx, &event);
// Assert: Sommelier records the maximized state + forwards to Exo.
EXPECT_EQ(window->maximized, 1);
EXPECT_CALL(
mock_wayland_channel_,
send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_SET_MAXIMIZED)))
.RetiresOnSaturation();
Pump();
// Act: Pretend the X11 client requests fullscreen.
xcb_client_message_event_t fsevent;
fsevent.response_type = XCB_CLIENT_MESSAGE;
fsevent.format = 32;
fsevent.window = window->id;
fsevent.type = ctx.atoms[ATOM_NET_WM_STATE].value;
fsevent.data.data32[0] = NET_WM_STATE_ADD;
fsevent.data.data32[1] = 0;
fsevent.data.data32[2] = ctx.atoms[ATOM_NET_WM_STATE_FULLSCREEN].value;
fsevent.data.data32[3] = 0;
fsevent.data.data32[4] = 0;
sl_handle_client_message(&ctx, &fsevent);
// Assert: Sommelier records the fullscreen state + forwards to Exo.
EXPECT_EQ(window->fullscreen, 1);
EXPECT_CALL(
mock_wayland_channel_,
send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_SET_FULLSCREEN)))
.RetiresOnSaturation();
Pump();
}
TEST_F(X11Test, UpdatesApplicationIdFromContext) {
sl_window* window = CreateToplevelWindow();
Pump();
window->managed = 1; // pretend window is mapped
// Should be ignored; global app id from context takes priority.
window->app_id_property = "org.chromium.guest_os.termina.appid.from.window";
ctx.application_id = "org.chromium.guest_os.termina.appid.from.context";
sl_update_application_id(&ctx, window);
EXPECT_CALL(mock_wayland_channel_,
send(AllOf(ExactlyOneMessage(AuraSurfaceId(window),
ZAURA_SURFACE_SET_APPLICATION_ID),
AnyMessageContainsString(ctx.application_id))))
.RetiresOnSaturation();
Pump();
}
TEST_F(X11Test, UpdatesApplicationIdFromWindow) {
sl_window* window = CreateToplevelWindow();
Pump();
window->managed = 1; // pretend window is mapped
window->app_id_property = "org.chromium.guest_os.termina.appid.from.window";
sl_update_application_id(&ctx, window);
EXPECT_CALL(mock_wayland_channel_,
send(AllOf(ExactlyOneMessage(AuraSurfaceId(window),
ZAURA_SURFACE_SET_APPLICATION_ID),
AnyMessageContainsString(window->app_id_property))))
.RetiresOnSaturation();
Pump();
}
TEST_F(X11Test, UpdatesApplicationIdFromWindowClass) {
sl_window* window = CreateToplevelWindow();
Pump();
window->managed = 1; // pretend window is mapped
window->clazz = strdup("very_classy"); // not const, can't use a literal
ctx.vm_id = "testvm";
sl_update_application_id(&ctx, window);
EXPECT_CALL(
mock_wayland_channel_,
send(AllOf(ExactlyOneMessage(AuraSurfaceId(window),
ZAURA_SURFACE_SET_APPLICATION_ID),
AnyMessageContainsString(
"org.chromium.guest_os.testvm.wmclass.very_classy"))))
.RetiresOnSaturation();
Pump();
free(window->clazz);
}
TEST_F(X11Test, UpdatesApplicationIdFromClientLeader) {
sl_window* window = CreateToplevelWindow();
Pump();
window->managed = 1; // pretend window is mapped
window->client_leader = window->id;
ctx.vm_id = "testvm";
sl_update_application_id(&ctx, window);
EXPECT_CALL(mock_wayland_channel_,
send(AllOf(ExactlyOneMessage(AuraSurfaceId(window),
ZAURA_SURFACE_SET_APPLICATION_ID),
AnyMessageContainsString(
"org.chromium.guest_os.testvm.wmclientleader."))))
.RetiresOnSaturation();
Pump();
}
TEST_F(X11Test, UpdatesApplicationIdFromXid) {
sl_window* window = CreateToplevelWindow();
Pump();
window->managed = 1; // pretend window is mapped
ctx.vm_id = "testvm";
sl_update_application_id(&ctx, window);
EXPECT_CALL(mock_wayland_channel_,
send(AllOf(ExactlyOneMessage(AuraSurfaceId(window),
ZAURA_SURFACE_SET_APPLICATION_ID),
AnyMessageContainsString(
"org.chromium.guest_os.testvm.xid."))))
.RetiresOnSaturation();
Pump();
}
TEST_F(X11Test, NonExistentWindowDoesNotCrash) {
// This test is testing cases where sl_lookup_window returns nullptr
// sl_handle_destroy_notify
xcb_destroy_notify_event_t destroy_event;
// Arrange: Use a window that does not exist.
destroy_event.window = 123;
// Act/Assert: Sommelier does not crash.
sl_handle_destroy_notify(&ctx, &destroy_event);
// sl_handle_client_message
xcb_client_message_event_t message_event;
message_event.window = 123;
message_event.data.data32[0] = WM_STATE_ICONIC;
message_event.type = ctx.atoms[ATOM_WL_SURFACE_ID].value;
sl_handle_client_message(&ctx, &message_event);
message_event.type = ctx.atoms[ATOM_NET_ACTIVE_WINDOW].value;
sl_handle_client_message(&ctx, &message_event);
message_event.type = ctx.atoms[ATOM_NET_WM_MOVERESIZE].value;
sl_handle_client_message(&ctx, &message_event);
message_event.type = ctx.atoms[ATOM_NET_WM_STATE].value;
sl_handle_client_message(&ctx, &message_event);
message_event.type = ctx.atoms[ATOM_WM_CHANGE_STATE].value;
sl_handle_client_message(&ctx, &message_event);
// sl_handle_map_request
xcb_map_request_event_t map_event;
map_event.window = 123;
sl_handle_map_request(&ctx, &map_event);
// sl_handle_unmap_notify
xcb_unmap_notify_event_t unmap_event;
unmap_event.window = 123;
unmap_event.response_type = 0;
sl_handle_unmap_notify(&ctx, &unmap_event);
// sl_handle_configure_request
xcb_configure_request_event_t configure_event;
configure_event.window = 123;
sl_handle_configure_request(&ctx, &configure_event);
// sl_handle_focus_in
xcb_focus_in_event_t focus_event;
focus_event.event = 123;
sl_handle_focus_in(&ctx, &focus_event);
// sl_handle_property_notify
xcb_property_notify_event_t notify_event;
notify_event.window = 123;
notify_event.atom = XCB_ATOM_WM_NAME;
sl_handle_property_notify(&ctx, &notify_event);
notify_event.atom = XCB_ATOM_WM_CLASS;
sl_handle_property_notify(&ctx, &notify_event);
notify_event.atom = ctx.application_id_property_atom;
sl_handle_property_notify(&ctx, &notify_event);
notify_event.atom = XCB_ATOM_WM_NORMAL_HINTS;
sl_handle_property_notify(&ctx, &notify_event);
notify_event.atom = XCB_ATOM_WM_HINTS;
sl_handle_property_notify(&ctx, &notify_event);
notify_event.atom = ATOM_MOTIF_WM_HINTS;
sl_handle_property_notify(&ctx, &notify_event);
notify_event.atom = ATOM_GTK_THEME_VARIANT;
sl_handle_property_notify(&ctx, &notify_event);
// sl_handle_reparent_notify
// Put this one last and used a different window id as it creates a window.
xcb_reparent_notify_event_t reparent_event;
reparent_event.window = 1234;
xcb_screen_t screen;
screen.root = 1234;
ctx.screen = &screen;
reparent_event.parent = ctx.screen->root;
reparent_event.x = 0;
reparent_event.y = 0;
sl_handle_reparent_notify(&ctx, &reparent_event);
}
#ifdef QUIRKS_SUPPORT
TEST_F(X11Test, IconifySuppressesFullscreen) {
// Arrange: Create an xdg_toplevel surface. Initially it's not iconified.
sl_window* window = CreateToplevelWindow();
uint32_t xdg_toplevel_id = XdgToplevelId(window);
EXPECT_EQ(window->iconified, 0);
window->steam_game_id = 123;
ctx.quirks.Load(
"sommelier { \n"
" condition { steam_game_id: 123 }\n"
" enable: FEATURE_BLACK_SCREEN_FIX\n"
"}");
// Act: Pretend an X11 client owns the surface, and requests to iconify it.
xcb_client_message_event_t event;
event.response_type = XCB_CLIENT_MESSAGE;
event.format = 32;
event.window = window->id;
event.type = ctx.atoms[ATOM_WM_CHANGE_STATE].value;
event.data.data32[0] = WM_STATE_ICONIC;
sl_handle_client_message(&ctx, &event);
Pump();
// Assert: Sommelier records the iconified state.
EXPECT_EQ(window->iconified, 1);
// Act: Pretend the surface is requested to be fullscreened.
event.type = ctx.atoms[ATOM_NET_WM_STATE].value;
event.data.data32[0] = NET_WM_STATE_ADD;
event.data.data32[1] = ctx.atoms[ATOM_NET_WM_STATE_FULLSCREEN].value;
event.data.data32[2] = 0;
event.data.data32[3] = 0;
event.data.data32[4] = 0;
sl_handle_client_message(&ctx, &event);
// Assert: Sommelier should not send the fullscreen call as we are iconified.
EXPECT_CALL(
mock_wayland_channel_,
send((ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_SET_FULLSCREEN))))
.Times(0);
Pump();
// Act: Pretend the surface receives focus.
xcb_focus_in_event_t focus_event;
focus_event.response_type = XCB_FOCUS_IN;
focus_event.event = window->id;
sl_handle_focus_in(&ctx, &focus_event);
// Assert: The window is deiconified.
EXPECT_EQ(window->iconified, 0);
// Assert: Sommelier should now send the fullscreen call.
EXPECT_CALL(
mock_wayland_channel_,
send((ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_SET_FULLSCREEN))))
.Times(1);
Pump();
}
TEST_F(X11Test, IconifySuppressesUnmaximize) {
// Arrange: Create an xdg_toplevel surface. Initially it's not iconified.
sl_window* window = CreateToplevelWindow();
uint32_t xdg_toplevel_id = XdgToplevelId(window);
EXPECT_EQ(window->iconified, 0);
window->steam_game_id = 123;
ctx.quirks.Load(
"sommelier { \n"
" condition { steam_game_id: 123 }\n"
" enable: FEATURE_BLACK_SCREEN_FIX\n"
"}");
// Arrange: Maximize it.
xcb_client_message_event_t event;
event.response_type = XCB_CLIENT_MESSAGE;
event.format = 32;
event.window = window->id;
event.type = ctx.atoms[ATOM_NET_WM_STATE].value;
event.data.data32[0] = NET_WM_STATE_ADD;
event.data.data32[1] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_VERT].value;
event.data.data32[2] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_HORZ].value;
event.data.data32[3] = 0;
event.data.data32[4] = 0;
sl_handle_client_message(&ctx, &event);
EXPECT_EQ(window->maximized, 1);
// Act: Pretend an X11 client owns the surface, and requests to iconify it.
event.type = ctx.atoms[ATOM_WM_CHANGE_STATE].value;
event.data.data32[0] = WM_STATE_ICONIC;
sl_handle_client_message(&ctx, &event);
Pump();
// Assert: Sommelier records the iconified state.
EXPECT_EQ(window->iconified, 1);
// Act: Pretend the surface is requested to be unmaximized.
event.type = ctx.atoms[ATOM_NET_WM_STATE].value;
event.data.data32[0] = NET_WM_STATE_REMOVE;
event.data.data32[1] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_VERT].value;
event.data.data32[2] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_HORZ].value;
event.data.data32[3] = 0;
event.data.data32[4] = 0;
sl_handle_client_message(&ctx, &event);
// Assert: Sommelier should not send the unmiximize call as we are iconified.
EXPECT_CALL(
mock_wayland_channel_,
send((ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_UNSET_MAXIMIZED))))
.Times(0);
Pump();
// Act: Pretend the surface receives focus.
xcb_focus_in_event_t focus_event;
focus_event.response_type = XCB_FOCUS_IN;
focus_event.event = window->id;
sl_handle_focus_in(&ctx, &focus_event);
// Assert: The window is deiconified.
EXPECT_EQ(window->iconified, 0);
// Assert: Sommelier should now send the unmiximize call.
EXPECT_CALL(
mock_wayland_channel_,
send((ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_UNSET_MAXIMIZED))))
.Times(1);
Pump();
}
#endif // QUIRKS_SUPPORT
// Matcher for the value_list argument of an X11 ConfigureWindow request,
// which is a const void* pointing to an int array whose size is implied by
// the flags argument.
MATCHER_P(ValueListMatches, expected, "") {
const int* value_ptr = static_cast<const int*>(arg);
std::vector<int> values;
for (std::vector<int>::size_type i = 0; i < expected.size(); i++) {
values.push_back(value_ptr[i]);
}
*result_listener << PrintToString(values);
return values == expected;
}
// Matcher for the data argument of an X11 ChangeProperty request,
// which is a const void* pointing to an uint32_t array whose size is implied by
// the flags argument. Hence, this does not test if argument exceeds the size of
// expected since we have to explicitly state the size in the function call.
MATCHER_P(ChangePropertyDataMatches, expected, "") {
const uint32_t* received = static_cast<const uint32_t*>(arg);
for (uint32_t i = 0; i < expected.size(); i++) {
if (received[i] != expected[i]) {
return false;
}
}
return true;
}
TEST_F(X11Test, XdgToplevelConfigureTriggersX11Configure) {
// Arrange
AdvertiseOutputs(xwayland.get(), {OutputConfig()});
sl_window* window = CreateToplevelWindow();
window->managed = 1; // pretend window is mapped
window->size_flags = 0; // no hinted position or size
const int width = 1024;
const int height = 768;
// Assert: Set up expectations for Sommelier to send appropriate X11 requests.
// (output width/height - width/height) / 2
int x = 448;
int y = 156;
EXPECT_CALL(xcb, configure_window(
testing::_, window->frame_id,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT |
XCB_CONFIG_WINDOW_BORDER_WIDTH,
ValueListMatches(std::vector({x, y, width, height, 0}))))
.Times(1);
EXPECT_CALL(xcb, configure_window(
testing::_, window->id,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT |
XCB_CONFIG_WINDOW_BORDER_WIDTH,
ValueListMatches(std::vector({0, 0, width, height, 0}))))
.Times(1);
// Act: Pretend the host compositor sends us some xdg configure events.
wl_array states;
wl_array_init(&states);
uint32_t* state =
static_cast<uint32_t*>(wl_array_add(&states, sizeof(uint32_t)));
*state = XDG_TOPLEVEL_STATE_ACTIVATED;
HostEventHandler(window->xdg_toplevel)
->configure(nullptr, window->xdg_toplevel, width, height, &states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, 123 /* serial */);
}
TEST_F(X11Test, XdgToplevelConfigureCentersWindowOnRotatedOutput) {
// Arrange
AdvertiseOutputs(xwayland.get(), {{.transform = WL_OUTPUT_TRANSFORM_90}});
sl_window* window = CreateToplevelWindow();
window->managed = 1; // pretend window is mapped
window->size_flags = 0; // no hinted position or size
const int width = 1024;
const int height = 768;
// Assert: Set up expectations for Sommelier to send appropriate X11 requests.
// (rotated output width/height - width/height) / 2
int x = 28;
int y = 576;
EXPECT_CALL(xcb, configure_window(
testing::_, window->frame_id,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT |
XCB_CONFIG_WINDOW_BORDER_WIDTH,
ValueListMatches(std::vector({x, y, width, height, 0}))))
.Times(1);
EXPECT_CALL(xcb, configure_window(
testing::_, window->id,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT |
XCB_CONFIG_WINDOW_BORDER_WIDTH,
ValueListMatches(std::vector({0, 0, width, height, 0}))))
.Times(1);
// Act: Pretend the host compositor sends us some xdg configure events.
wl_array states;
wl_array_init(&states);
uint32_t* state =
static_cast<uint32_t*>(wl_array_add(&states, sizeof(uint32_t)));
*state = XDG_TOPLEVEL_STATE_ACTIVATED;
HostEventHandler(window->xdg_toplevel)
->configure(nullptr, window->xdg_toplevel, width, height, &states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, 123 /* serial */);
}
TEST_F(X11Test,
XdgToplevelConfigureCentersWindowCorrectlyWhenMultipleOutputsExist) {
// Arrange
AdvertiseOutputs(
xwayland.get(),
{{.x = 0, .y = 0, .width_pixels = 1920, .height_pixels = 1080},
{.x = 1920, .y = 500}});
sl_window* window = CreateToplevelWindow();
window->managed = 1; // pretend window is mapped
window->size_flags = 0; // no hinted position or size
const int width = 1024;
const int height = 768;
struct sl_host_output* output = nullptr;
output = ctx.host_outputs[1];
HostEventHandler(window->paired_surface->proxy)
->enter(nullptr, window->paired_surface->proxy, output->proxy);
// Assert: Set up expectations for Sommelier to send appropriate X11 requests.
int x = 1920 + 448;
int y = 156;
EXPECT_CALL(xcb, configure_window(
testing::_, window->frame_id,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT |
XCB_CONFIG_WINDOW_BORDER_WIDTH,
ValueListMatches(std::vector({x, y, width, height, 0}))))
.Times(1);
EXPECT_CALL(xcb, configure_window(
testing::_, window->id,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT |
XCB_CONFIG_WINDOW_BORDER_WIDTH,
ValueListMatches(std::vector({0, 0, width, height, 0}))))
.Times(1);
// Act: Pretend the host compositor sends us some xdg configure events.
wl_array states;
wl_array_init(&states);
uint32_t* state =
static_cast<uint32_t*>(wl_array_add(&states, sizeof(uint32_t)));
*state = XDG_TOPLEVEL_STATE_ACTIVATED;
HostEventHandler(window->xdg_toplevel)
->configure(nullptr, window->xdg_toplevel, width, height, &states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, 123 /* serial */);
}
TEST_F(X11DirectScaleTest, AuraToplevelConfigureTriggersX11Configure) {
// Arrange
ctx.enable_x11_move_windows = true;
AdvertiseOutputs(xwayland.get(), {OutputConfig()});
sl_window* window = CreateToplevelWindow();
window->managed = 1; // pretend window is mapped
window->size_flags = 0; // no hinted position or size
const int x = 50;
const int y = 60;
const int width = 1024;
const int height = 768;
// Assert: Set up expectations for Sommelier to send appropriate X11 requests.
EXPECT_CALL(xcb, configure_window(
testing::_, window->frame_id,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT |
XCB_CONFIG_WINDOW_BORDER_WIDTH,
ValueListMatches(std::vector({x, y, width, height, 0}))))
.Times(1);
EXPECT_CALL(xcb, configure_window(
testing::_, window->id,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT |
XCB_CONFIG_WINDOW_BORDER_WIDTH,
ValueListMatches(std::vector({0, 0, width, height, 0}))))
.Times(1);
// Act: Pretend the host compositor sends us some configure events.
wl_array states;
wl_array_init(&states);
uint32_t* state =
static_cast<uint32_t*>(wl_array_add(&states, sizeof(uint32_t)));
*state = XDG_TOPLEVEL_STATE_ACTIVATED;
HostEventHandler(window->aura_toplevel)
->configure(nullptr, window->aura_toplevel, x, y, width, height, &states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, 123 /* serial */);
}
TEST_F(X11Test, AuraToplevelOriginChangeTriggersX11Configure) {
// Arrange
ctx.enable_x11_move_windows = true;
AdvertiseOutputs(xwayland.get(), {OutputConfig()});
sl_window* window = CreateToplevelWindow();
window->managed = 1; // pretend window is mapped
window->size_flags = 0; // no hinted position or size
const int x = 50;
const int y = 60;
// Assert
EXPECT_CALL(xcb, configure_window(testing::_, window->frame_id,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y,
ValueListMatches(std::vector({x, y}))))
.Times(1);
// Act
HostEventHandler(window->aura_toplevel)
->origin_change(nullptr, window->aura_toplevel, x, y);
}
// When the host compositor sends a window position, make sure we don't send
// a bounds request back. Otherwise we get glitching due to rounding and race
// conditions.
TEST_F(X11Test, AuraToplevelOriginChangeDoesNotRoundtrip) {
// Arrange
ctx.enable_x11_move_windows = true;
AdvertiseOutputs(xwayland.get(), {OutputConfig()});
sl_window* window = CreateToplevelWindow();
window->managed = 1; // pretend window is mapped
window->size_flags = 0; // no hinted position or size
const int x = 50;
const int y = 60;
// Assert: set_window_bounds() never sent.
EXPECT_CALL(mock_wayland_channel_,
send(testing::Not(AtLeastOneMessage(
AuraToplevelId(window), ZAURA_TOPLEVEL_SET_WINDOW_BOUNDS))))
.Times(testing::AtLeast(0));
// Act
HostEventHandler(window->aura_toplevel)
->origin_change(nullptr, window->aura_toplevel, x, y);
Pump();
}
TEST_F(X11Test, X11ConfigureRequestPositionIsForwardedToAuraHost) {
// Arrange
ctx.enable_x11_move_windows = true;
AdvertiseOutputs(xwayland.get(), {OutputConfig()});
sl_window* window = CreateToplevelWindow();
window->managed = 1; // pretend window is mapped
Pump(); // discard Wayland requests sent in setup
// Assert
EXPECT_CALL(mock_wayland_channel_,
send(AtLeastOneMessage(AuraToplevelId(window),
ZAURA_TOPLEVEL_SET_WINDOW_BOUNDS)))
.RetiresOnSaturation();
// Act
xcb_configure_request_event_t configure = {
.response_type = XCB_CONFIGURE_REQUEST,
.sequence = 123,
.parent = window->frame_id,
.window = window->id,
.x = 10,
.y = 20,
.width = 300,
.height = 400,
.value_mask = XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT};
sl_handle_configure_request(&ctx, &configure);
Pump();
}
TEST_F(X11Test,
X11ConfigureRequestPositionForwardingIgnoresStaleAuraToplevelConfigure) {
// Arrange
ctx.enable_x11_move_windows = true;
AdvertiseOutputs(xwayland.get(), {OutputConfig()});
sl_window* window = CreateToplevelWindow();
window->managed = 1; // pretend window is mapped
const int width = 300;
const int height = 400;
// Position requested by the client.
const int client_requested_x = 10;
const int client_requested_y = 0;
// Stale position received from the host compositor.
const int stale_x = 50;
const int stale_y = 60;
// Host compositor's adjusted response to the client's request.
// (In this scenario, it moved the window down so its server-side
// decorations wouldn't be offscreen.)
const int granted_x = client_requested_x;
const int granted_y = 32;
//
// Assert
//
// Barrier should prevent forwarding the host's stale coords to the X server.
EXPECT_CALL(
xcb, configure_window(testing::_, window->frame_id, testing::_,
ValueListMatches(std::vector({stale_x, stale_y}))))
.Times(0);
// Do forward the correct coordinates to the X server.
EXPECT_CALL(xcb, configure_window(
testing::_, window->frame_id,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT |
XCB_CONFIG_WINDOW_BORDER_WIDTH,
ValueListMatches(std::vector(
{granted_x, granted_y, width, height, 0}))))
.Times(1);
// The reparented child window may also get configured.
// The details are not important for this test case.
EXPECT_CALL(xcb,
configure_window(testing::_, window->id, testing::_, testing::_))
.Times(testing::AtLeast(0));
//
// Act
//
// An incoming ConfigureRequest sends set_window_bounds(), and sets up the
// event barrier.
xcb_configure_request_event_t configure = {
.response_type = XCB_CONFIGURE_REQUEST,
.sequence = 123,
.parent = window->frame_id,
.window = window->id,
.x = client_requested_x,
.y = client_requested_y,
.width = width,
.height = height,
.value_mask = XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT};
sl_handle_configure_request(&ctx, &configure);
EXPECT_NE(window->configure_event_barrier, nullptr);
// Meanwhile, host compositor is sending stale position data, both via the
// regular configure sequence and via origin_change events.
wl_array states;
wl_array_init(&states);
uint32_t* state =
static_cast<uint32_t*>(wl_array_add(&states, sizeof(uint32_t)));
*state = XDG_TOPLEVEL_STATE_ACTIVATED;
uint32_t serial = 120;
HostEventHandler(window->aura_toplevel)
->configure(nullptr, window->aura_toplevel, stale_x, stale_y, width,
height, &states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, serial++);
HostEventHandler(window->aura_toplevel)
->origin_change(nullptr, window->aura_toplevel, stale_x, stale_y);
Pump();
// Exo catches up to the set_window_bounds() request. It modifies the
// requested coords slightly and returns them in a fresh configure sequence.
HostEventHandler(window->aura_toplevel)
->configure(nullptr, window->aura_toplevel, granted_x, granted_y, width,
height, &states);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, serial++);
// Exo catches up to the event barrier.
HostEventHandler(window->configure_event_barrier)
->done(nullptr, window->configure_event_barrier, serial++);
Pump();
}
TEST_F(X11Test, X11ConfigureRequestWithoutPositionIsNotForwardedToAuraHost) {
// Arrange
ctx.enable_x11_move_windows = true;
AdvertiseOutputs(xwayland.get(), {OutputConfig()});
sl_window* window = CreateToplevelWindow();
window->managed = 1; // pretend window is mapped
// Assert: set_window_bounds() never sent.
EXPECT_CALL(mock_wayland_channel_,
send(testing::Not(AtLeastOneMessage(
AuraToplevelId(window), ZAURA_TOPLEVEL_SET_WINDOW_BOUNDS))))
.Times(testing::AtLeast(0));
// Act
xcb_configure_request_event_t configure = {
.response_type = XCB_CONFIGURE_REQUEST,
.sequence = 123,
.parent = window->frame_id,
.window = window->id,
.width = 300,
.height = 400,
.value_mask = XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT};
sl_handle_configure_request(&ctx, &configure);
Pump();
}
TEST_F(X11Test, X11OnlyClientCanExitFullScreenIfFlagSet) {
// Arrange
AdvertiseOutputs(xwayland.get(), {OutputConfig()});
sl_window* window = CreateToplevelWindow();
// pretend window is mapped and fullscreen
window->managed = 1;
window->fullscreen = true;
window->compositor_fullscreen = true;
window->allow_resize = false;
// No states from Wayland - which means non-fullscreen
struct wl_array states;
wl_array_init(&states);
// Test with the flag set. X11 next_config.state is set to fullscreen to
// retain fullscreen.
ctx.only_client_can_exit_fullscreen = true;
window->next_config.states_length = 0;
HostEventHandler(window->xdg_toplevel)
->configure(nullptr, window->xdg_toplevel, 800, 600, &states);
// window->fullscreen is not updated by sl_internal_toplevel_configure.
// It is done by X11 notifying property change, hence we only test
// compositor_fullscreen here.
ASSERT_EQ(window->next_config.states_length, 1);
ASSERT_EQ(window->next_config.states[0],
window->ctx->atoms[ATOM_NET_WM_STATE_FULLSCREEN].value);
ASSERT_FALSE(window->compositor_fullscreen);
ASSERT_FALSE(window->allow_resize);
}
TEST_F(X11Test, X11ExitFullScreenIfFlagNotSet) {
// Arrange
AdvertiseOutputs(xwayland.get(), {OutputConfig()});
sl_window* window = CreateToplevelWindow();
// pretend window is mapped and fullscreen
window->managed = 1;
window->fullscreen = true;
window->compositor_fullscreen = true;
window->allow_resize = false;
// No states from Wayland - which means non-fullscreen
struct wl_array states;
wl_array_init(&states);
// Test with without flag set. X11 next_config.state no longer includes
// fullscreen.
ctx.only_client_can_exit_fullscreen = false;
window->next_config.states_length = 0;
HostEventHandler(window->xdg_toplevel)
->configure(nullptr, window->xdg_toplevel, 800, 600, &states);
// Compositor will treat the window as non-fullscreen and allow resize.
// Since no next_config is defined, follow up xcb()->change_property()
// will be called without any states - indicating the window is no
// longer fullscreen.
ASSERT_EQ(window->next_config.states_length, 0);
ASSERT_FALSE(window->compositor_fullscreen);
ASSERT_TRUE(window->allow_resize);
}
TEST_F(X11Test, X11NextConfigFullscreenStateConsumedCorrectly) {
// Arrange
AdvertiseOutputs(xwayland.get(), {OutputConfig()});
sl_window* window = CreateToplevelWindow();
// pretend window is mapped and fullscreen
window->managed = 1;
window->fullscreen = true;
window->compositor_fullscreen = true;
window->allow_resize = false;
// set fullscreen
window->next_config.states_length = 1;
window->next_config.states[0] =
window->ctx->atoms[ATOM_NET_WM_STATE_FULLSCREEN].value;
EXPECT_CALL(xcb, change_property(
testing::_, XCB_PROP_MODE_REPLACE, window->id,
ctx.atoms[ATOM_NET_WM_STATE].value, XCB_ATOM_ATOM, 32, 1,
ChangePropertyDataMatches(std::vector(
{ctx.atoms[ATOM_NET_WM_STATE_FULLSCREEN].value}))))
.Times(1);
HostEventHandler(window->xdg_surface)
->configure(nullptr, window->xdg_surface, 0);
Pump();
}
} // namespace sommelier
} // namespace vm_tools