tests: cover quit dialog, constraint/text-input/dialog teardown, virtual abs+mods, mgmt probe

This commit is contained in:
Andrew Hamilton 2026-09-05 10:59:49 +10:00
parent 32de4866a6
commit ca7f1f2852
3 changed files with 285 additions and 15 deletions

4
kick.c
View file

@ -8465,6 +8465,10 @@ static void constraint_deactivate(void) {
static void handle_constraint_destroy(struct wl_listener *listener, void *data) {
(void)data;
struct kick_server *s = wl_container_of(listener, s, constraint_destroy);
kick_log(KICK_LOG_DEBUG, "[constraint] destroyed\n");
/* Detach like constraint_deactivate does: wlroots asserts every
listener is gone before it frees the constraint. */
wl_list_remove(&s->constraint_destroy.link);
s->active_constraint = NULL;
}

View file

@ -78,6 +78,9 @@ M = 50
U = 22
V = 47
R = 19
C = 46
D = 32
G = 34
ESC = 1
ENTER = 28
UP = 103
@ -943,6 +946,27 @@ def test_standalone_headless_locked_pointer_holds():
s.close()
def test_standalone_headless_locked_pointer_release():
"""Dropping a locked constraint mid-session tears it down: the destroy
path runs and the cursor is free to move again."""
s = _spawn_standalone({}, "%s --constrain locked '#ff0000'" % WL)
try:
s.wait_app_active(0)
s.wait_active_frame()
assert s.proc.poll() is None
s.warp(300, 300)
assert _wait_log(s, r"\[constraint\] locked on surface",
timeout=5.0), \
"locked constraint never activated"
s.tap(C)
assert _wait_log(s, r"\[constraint\] destroyed", timeout=5.0), \
"constraint destroy never ran"
assert s.proc.poll() is None, \
f"compositor died on constraint release; log tail:\n{s.read_log()[-400:]}"
finally:
s.close()
def test_standalone_headless_confined_pointer_clamps():
"""A CONFINED pointer constraint keeps the visible cursor inside the
constraint region even when relative motion would carry it outside."""
@ -1284,6 +1308,22 @@ def test_standalone_headless_text_input_roundtrip(tmp_path):
s.close()
def test_standalone_headless_text_input_disable():
"""Disabling an enabled text field mid-session runs the compositor's
disable relay without disturbing the session."""
s = _spawn_standalone(
{}, "%s --text-input '#ff0000'" % WL)
try:
s.wait_app_active(0)
s.wait_surface_mapped(0)
s.tap(D)
time.sleep(1.0)
assert s.proc.poll() is None, \
f"compositor died on text-input disable; log tail:\n{s.read_log()[-400:]}"
finally:
s.close()
def test_standalone_headless_text_input_focus_follows_tab(tmp_path):
"""Cycling-free focus follow: when the focused field's tab closes, focus
hands to the left neighbor, which is entered and gets its own IME
@ -1470,6 +1510,28 @@ def test_standalone_headless_modal_steals_tab(tmp_path):
s.close()
def test_standalone_headless_modal_dialog_destroy_unwires():
"""Destroying a modal dialog's object mid-session unwires it without
disturbing the session: the compositor drops the modal wiring and the
window carries on as a normal tab."""
s = _spawn_standalone(
{},
"%s --dialog-modal --app-id dlg '#0000ff'" % WL,
"%s '#ff0000'" % WL)
try:
s.wait_any_active()
s.wait_for(r"\[dialog\] modal app=0\b")
# The modal steal focuses its tab (possibly before startup
# settles); the key must reach that client.
s.wait_active(0)
s.tap(G)
time.sleep(1.0)
assert s.proc.poll() is None, \
f"compositor died on dialog destroy; log tail:\n{s.read_log()[-400:]}"
finally:
s.close()
def test_standalone_headless_modal_tab_groups_off(tmp_path):
"""With --tab-groups off a background modal still takes its (own) tab:
it is glued after its parent and activated on map."""
@ -1599,6 +1661,32 @@ def test_standalone_headless_output_mgmt_list(tmp_path):
s.close()
def test_standalone_headless_output_mgmt_test(tmp_path):
"""Probing 640x480 through output-management succeeds without
resizing: the client sees before/tested and the output keeps its
size."""
out = tmp_path / "om.out"
s = _spawn_standalone(
{},
"%s --output-mgmt --output-mgmt-out '%s' --output-mgmt-size 640x480 "
"--output-mgmt-test" % (WL, out),
"%s '#ff0000'" % WL)
try:
s.wait_any_active()
deadline = time.monotonic() + 20.0
while time.monotonic() < deadline:
if out.exists() and "tested" in out.read_text():
break
time.sleep(0.2)
text = out.read_text() if out.exists() else ""
assert "before " in text, f"no before line in: {text!r}"
assert "tested" in text, f"test not acked in: {text!r}"
assert "after " not in text, f"probe resized the output: {text!r}"
assert s.proc.poll() is None, f"log tail:\n{s.read_log()[-400:]}"
finally:
s.close()
def test_standalone_headless_output_mgmt_apply(tmp_path):
"""Applying 800x600 through output-management resizes the session:
the client sees before/applied/after and a tab commits at 800x600."""
@ -2910,6 +2998,31 @@ def test_quit_confirm_multi_window():
s.proc.terminate()
def test_quit_confirm_dialog_renders():
"""The confirm-quit modal actually draws: with the question pending, a
frame dump shows the dark dialog box over the client content."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
s = _nested_session(f"{WL} '{RED}'", f"{WL} '{GREEN}'")
try:
s.wait_any_active()
s.wait_for(r"\[claim\] app=0\b")
s.wait_for(r"\[claim\] app=1\b")
s.chord(CTRL | ALT, Q)
s.wait_for(r"\[confirm\] quit: 2 windows open ")
w, h, px = s.ppm_rgb(s.dump_host())
assert _has_dark(w, h, px), \
"confirm-quit dialog not visible in the composed frame"
s.tap(N) # keep running
s.wait_for(r"kick: quit cancelled\b")
assert s.proc.poll() is None, \
"compositor exited after cancelling quit"
finally:
s.close()
if s.proc.poll() is None:
s.proc.terminate()
def test_quit_cancel_keeps_running():
"""Ctrl+Alt+Q confirmation can be declined: N and Escape both dismiss it,
the compositor keeps running, and a later quit request re-asks."""
@ -4841,6 +4954,26 @@ def _oneshot_virtual_motion(sock, dx, dy, click=False):
pass
def _oneshot_virtual_abs(sock, x, y, click=False):
"""Run a one-shot --virtual-abs (output-fraction warp) plus optional
click against a live socket."""
args = [WL, "--virtual-abs", str(x), str(y)]
if click:
args.append("--virtual-click")
p = subprocess.Popen(
args, env=dict(os.environ, WAYLAND_DISPLAY=sock),
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, start_new_session=True,
preexec_fn=_preexec_death(os.getpid()))
try:
p.wait(timeout=10.0)
finally:
try:
p.kill()
except Exception:
pass
def test_standalone_headless_virtual_pointer_motion_click(tmp_path):
"""A virtual pointer moves the cursor and clicks: the focused window
receives pointer motion and button events through the seat."""
@ -4882,6 +5015,72 @@ def test_standalone_headless_virtual_pointer_motion_click(tmp_path):
s.close()
def test_standalone_headless_virtual_pointer_absolute(tmp_path):
"""A virtual pointer warps absolutely: the focused window sees a
pointer enter at the fraction position plus the click."""
out = tmp_path / "pointer.out"
s = _spawn_standalone(
{}, "%s --app-id testwin --pointer-out '%s' '#ff0000' 320x240"
% (WL, out))
try:
s.wait_any_active()
s.wait_surface_mapped(0)
# Centre of the output is well inside the maximized window.
_oneshot_virtual_abs(s.socket, 0.5, 0.5, click=True)
deadline = time.monotonic() + 10.0
got_enter = got_button = False
while time.monotonic() < deadline:
if out.exists():
text = out.read_text()
got_enter = "enter " in text
got_button = "button 272 " in text
if got_enter and got_button:
break
time.sleep(0.1)
assert got_enter, \
f"window saw no pointer enter from abs warp: " \
f"{(out.read_text() if out.exists() else '(missing)')!r}"
assert got_button, \
f"window saw no button press after abs warp: " \
f"{(out.read_text() if out.exists() else '(missing)')!r}"
assert s.proc.poll() is None, \
f"compositor exited on virtual abs pointer; log tail:\n{s.read_log()[-400:]}"
finally:
s.close()
def test_standalone_headless_virtual_keyboard_modifiers():
"""Explicit virtual-keyboard modifiers reach the seat without pressing
any key and without disturbing the session."""
s = _spawn_standalone(
{}, "%s --app-id win0 '#ff0000' 320x240" % WL,
"%s --app-id win1 '#00ff00' 320x240" % WL)
try:
start = s.wait_any_active()
p = subprocess.Popen(
[WL, "--virtual-modifiers", "1"],
env=dict(os.environ, WAYLAND_DISPLAY=s.socket),
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, start_new_session=True,
preexec_fn=_preexec_death(os.getpid()))
try:
p.wait(timeout=10.0)
finally:
try:
p.kill()
except Exception:
pass
assert p.returncode == 0, \
f"virtual-modifiers oneshot failed: rc={p.returncode}"
time.sleep(0.5)
# Modifiers alone switch nothing and kill nothing.
assert s.active() == start, "lone modifiers changed the active tab"
assert s.proc.poll() is None, \
f"compositor exited on virtual modifiers; log tail:\n{s.read_log()[-400:]}"
finally:
s.close()
def test_standalone_headless_xdg_activation_globals(tmp_path):
"""The compositor advertises xdg_activation_v1 so launchers can focus
windows by token."""

View file

@ -85,6 +85,9 @@ static struct zwp_locked_pointer_v1 *locked_ptr;
static struct zwp_confined_pointer_v1 *confined_ptr;
static struct wl_keyboard *keyboard;
static struct wl_pointer *pointer;
/* Forward declaration for the 'D' key handler below: the text-input
object is defined with the rest of the text-input mode further down. */
static struct zwp_text_input_v3 *ti_obj;
/* --touch-log: append every received touch event to this file so tests can
prove the compositor delivered touchscreen input end-to-end. */
static FILE *touch_log;
@ -428,6 +431,7 @@ static bool want_extra;
extra one. --extra-delay <ms> sleeps before opening the extra window
so boot settles first (lets a modal steal a settled tab). */
static bool dialog_modal;
static struct xdg_dialog_v1 *dialog_obj;
static bool extra_modal;
static int extra_delay_ms;
static bool extra_done;
@ -536,6 +540,30 @@ static void keyboard_key(void *data, struct wl_keyboard *kb, uint32_t serial,
ex_xdg = NULL;
ex_surface = NULL;
}
/* 'G' (evdev 34) destroys the xdg-dialog object, letting tests prove
the compositor unwires a modal dialog mid-session. */
if (key == 34 && state == WL_KEYBOARD_KEY_STATE_PRESSED && dialog_obj) {
xdg_dialog_v1_destroy(dialog_obj);
dialog_obj = NULL;
}
/* 'D' (evdev 32) disables the text-input field, letting tests prove
the compositor relays the disable to the input method. */
if (key == 32 && state == WL_KEYBOARD_KEY_STATE_PRESSED && ti_obj) {
zwp_text_input_v3_disable(ti_obj);
zwp_text_input_v3_commit(ti_obj);
}
/* 'C' (evdev 46) drops the pointer constraint, letting tests prove
the compositor tears the active constraint down mid-session. */
if (key == 46 && state == WL_KEYBOARD_KEY_STATE_PRESSED) {
if (locked_ptr) {
zwp_locked_pointer_v1_destroy(locked_ptr);
locked_ptr = NULL;
}
if (confined_ptr) {
zwp_confined_pointer_v1_destroy(confined_ptr);
confined_ptr = NULL;
}
}
if (state == WL_KEYBOARD_KEY_STATE_PRESSED)
maybe_set_primary(serial);
}
@ -1831,9 +1859,11 @@ static void gesture_bind_all(void) {
/* --- Output management (wlr-output-management) ---
--output-mgmt --output-mgmt-out <file> [--output-mgmt-size WxH]:
list the head (name + current size) into <file>; with a size, apply
it as a custom mode first and record before/after/applied. No window
is opened. */
it as a custom mode first and record before/after/applied.
--output-mgmt-test probes instead of applying (records "tested").
No window is opened. */
static bool output_mgmt_mode;
static bool output_mgmt_test_only;
static const char *output_mgmt_out;static int output_mgmt_w, output_mgmt_h;
static bool output_mgmt_want_size;
static struct zwlr_output_head_v1 *om_head;
@ -1883,7 +1913,9 @@ static void om_report_head(const char *tag) {
static void om_config_succeeded(void *data,
struct zwlr_output_configuration_v1 *cfg) {
(void)data; (void)cfg;
om_note("applied");
om_note(output_mgmt_test_only ? "tested" : "applied");
if (output_mgmt_test_only)
exit(0);
om_applied = true;
}
@ -2032,7 +2064,10 @@ static void om_mgr_done(void *data, struct zwlr_output_manager_v1 *mgr,
zwlr_output_configuration_v1_enable_head(cfg, om_head);
zwlr_output_configuration_head_v1_set_custom_mode(
chead, output_mgmt_w, output_mgmt_h, 0);
zwlr_output_configuration_v1_apply(cfg);
if (output_mgmt_test_only)
zwlr_output_configuration_v1_test(cfg);
else
zwlr_output_configuration_v1_apply(cfg);
wl_display_flush(display);
return;
}
@ -2220,9 +2255,12 @@ static void power_run(void) {
/* --- Virtual keyboard (zwp_virtual_keyboard_v1) ---
--virtual-key <code:state,...>: synthesise key events through a virtual
keyboard (no keymap sent; the compositor installs its own). E.g.
"56:1,103:1,103:0,56:0" presses Alt+Up (tab-cycle) and releases. Exits
after flushing. */
"56:1,103:1,103:0,56:0" presses Alt+Up (tab-cycle) and releases.
--virtual-modifiers <mask> sends an explicit modifier state (depressed
mask) with no keys. Exits after flushing. */
static const char *virtual_key_spec;
static uint32_t virtual_mods;
static bool virtual_mods_set;
static void virtual_key_run(void) {
display = wl_display_connect(NULL);
@ -2271,6 +2309,8 @@ static void virtual_key_run(void) {
xkb_context_unref(xkb_ctx);
char *spec = strdup(virtual_key_spec ? virtual_key_spec : "");
uint32_t time = 1000;
if (virtual_mods_set)
zwp_virtual_keyboard_v1_modifiers(vkb, virtual_mods, 0, 0, 0);
for (char *tok = strtok(spec, ","); tok; tok = strtok(NULL, ",")) {
unsigned code = 0, state = 0;
if (sscanf(tok, "%u:%u", &code, &state) == 2)
@ -2283,9 +2323,11 @@ static void virtual_key_run(void) {
/* --- Virtual pointer (zwlr_virtual_pointer_v1) ---
--virtual-motion <dx> <dy> [--virtual-click]: move the pointer relatively
by (dx,dy); with --virtual-click also press+release left button there.
Exits after flushing. */
by (dx,dy); --virtual-abs <x> <y> moves absolutely to the (x,y) fraction
of the output instead. With --virtual-click also press+release left
button there. Exits after flushing. */
static double virtual_dx, virtual_dy;
static double virtual_abs_x = -1.0, virtual_abs_y = -1.0;
static bool virtual_click;
static void virtual_motion_run(void) {
@ -2304,8 +2346,14 @@ static void virtual_motion_run(void) {
struct zwlr_virtual_pointer_v1 *vptr =
zwlr_virtual_pointer_manager_v1_create_virtual_pointer(
virtual_ptr_mgr, seat);
zwlr_virtual_pointer_v1_motion(vptr, 1000,
wl_fixed_from_double(virtual_dx), wl_fixed_from_double(virtual_dy));
if (virtual_abs_x >= 0.0 && virtual_abs_y >= 0.0)
zwlr_virtual_pointer_v1_motion_absolute(vptr, 1000,
wl_fixed_from_double(virtual_abs_x * 1000.0),
wl_fixed_from_double(virtual_abs_y * 1000.0), 1000, 1000);
else
zwlr_virtual_pointer_v1_motion(vptr, 1000,
wl_fixed_from_double(virtual_dx),
wl_fixed_from_double(virtual_dy));
if (virtual_click) {
/* BTN_LEFT down/up at the new position. */
zwlr_virtual_pointer_v1_button(vptr, 1010, 272, 1);
@ -2705,7 +2753,6 @@ static void demand_primary_once(void) {
compositor relays field -> IME -> field end to end. */
static bool text_input_mode;
static const char *text_input_out;
static struct zwp_text_input_v3 *ti_obj;
static bool ti_enabled;
static void ti_note_commit(const char *kind, const char *text) {
@ -2955,7 +3002,8 @@ static void print_usage(const char *argv0) {
" logging it to --popup-out when given\n"
" --output-mgmt lists the output head (name + size)\n"
" to --output-mgmt-out, applying --output-mgmt-size\n"
" first when given",
" first when given (--output-mgmt-test probes it\n"
" instead, recording \"tested\")",
argv0, argv0);
}
@ -3053,6 +3101,9 @@ int main(int argc, char **argv) {
}
output_mgmt_want_size = true;
i += 2;
} else if (strcmp(argv[i], "--output-mgmt-test") == 0) {
output_mgmt_test_only = true;
i += 1;
} else if (strcmp(argv[i], "--icon") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --icon needs a color\n");
@ -3375,6 +3426,22 @@ int main(int argc, char **argv) {
} else if (strcmp(argv[i], "--virtual-click") == 0) {
virtual_click = true;
i += 1;
} else if (strcmp(argv[i], "--virtual-abs") == 0) {
if (i + 2 >= argc) {
fprintf(stderr, "wltest: --virtual-abs needs x y fractions\n");
return 1;
}
virtual_abs_x = atof(argv[i + 1]);
virtual_abs_y = atof(argv[i + 2]);
i += 3;
} else if (strcmp(argv[i], "--virtual-modifiers") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --virtual-modifiers needs a mask\n");
return 1;
}
virtual_mods = (uint32_t)strtoul(argv[i + 1], NULL, 0);
virtual_mods_set = true;
i += 2;
} else if (strcmp(argv[i], "--activate") == 0) {
activate_mode = true;
i += 1;
@ -3487,7 +3554,7 @@ int main(int argc, char **argv) {
return 0;
}
/* Virtual keyboard: synthesise keys, then exit. */
if (virtual_key_spec) {
if (virtual_key_spec || virtual_mods_set) {
virtual_key_run();
return 0;
}
@ -3727,9 +3794,9 @@ int main(int argc, char **argv) {
fprintf(stderr, "wltest: dialog-modal: xdg_wm_dialog_v1 missing\n");
return 1;
}
struct xdg_dialog_v1 *d =
dialog_obj =
xdg_wm_dialog_v1_get_xdg_dialog(dialog_mgr, toplevel);
xdg_dialog_v1_set_modal(d);
xdg_dialog_v1_set_modal(dialog_obj);
}
/* Tearing hint for the window; fractional scale is reported back