Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ jobs:
env:
DESTDIR: out
run: |
meson build -Ddocumentation=true
meson build -Ddocumentation=true -Dtests=true
ninja -C build
ninja -C build install
- name: Run Tests
run: |
meson test -v -C build

fedora:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -93,3 +96,4 @@ jobs:
io.elementary.vala-lint -d lib
io.elementary.vala-lint -d plugins
io.elementary.vala-lint -d src
io.elementary.vala-lint -d tests
30 changes: 14 additions & 16 deletions lib/Gestures/GestureController.vala
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ public class Gala.GestureController : Object {

public bool recognizing { get; private set; }

private ToucheggBackend? touchegg_backend;
private TouchpadBackend? touchpad_backend;
private List<GestureBackend> enabled_backends = new List<GestureBackend> ();

private ScrollBackend? scroll_backend;

private GestureBackend? recognizing_backend;
Expand Down Expand Up @@ -121,26 +121,24 @@ public class Gala.GestureController : Object {

public void enable_touchpad (Clutter.Actor actor) {
if (Meta.Util.is_wayland_compositor ()) {
touchpad_backend = new TouchpadBackend (actor, group);
touchpad_backend.on_gesture_detected.connect (gesture_detected);
touchpad_backend.on_begin.connect (gesture_begin);
touchpad_backend.on_update.connect (gesture_update);
touchpad_backend.on_end.connect (gesture_end);
enable_backend (new TouchpadBackend (actor, group));
}

touchegg_backend = ToucheggBackend.get_default (); // Will automatically filter events on wayland
touchegg_backend.on_gesture_detected.connect (gesture_detected);
touchegg_backend.on_begin.connect (gesture_begin);
touchegg_backend.on_update.connect (gesture_update);
touchegg_backend.on_end.connect (gesture_end);
enable_backend (ToucheggBackend.get_default ()); // Will automatically filter events on wayland
}

public void enable_scroll (Clutter.Actor actor, Clutter.Orientation orientation) {
scroll_backend = new ScrollBackend (actor, orientation, new GestureSettings ());
scroll_backend.on_gesture_detected.connect (gesture_detected);
scroll_backend.on_begin.connect (gesture_begin);
scroll_backend.on_update.connect (gesture_update);
scroll_backend.on_end.connect (gesture_end);
enable_backend (scroll_backend);
}

internal void enable_backend (GestureBackend backend) {
backend.on_gesture_detected.connect (gesture_detected);
backend.on_begin.connect (gesture_begin);
backend.on_update.connect (gesture_update);
backend.on_end.connect (gesture_end);

enabled_backends.append (backend);
}

private void prepare () {
Expand Down
3 changes: 3 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ subdir('plugins/template')
if get_option('documentation')
subdir('docs')
endif
if get_option('tests')
subdir('tests')
endif
subdir('po')

vapigen = find_program('vapigen', required: false)
Expand Down
1 change: 1 addition & 0 deletions meson_options.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
option ('documentation', type : 'boolean', value : false)
option ('tests', type : 'boolean', value : false)
option ('systemd', type : 'boolean', value : true)
option ('systemduserunitdir', type : 'string', value : '')
43 changes: 43 additions & 0 deletions tests/MockWindowManager.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2025 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*/

public class Gala.MockWindowManager : Meta.Plugin, WindowManager {
public Clutter.Actor ui_group { get; protected set; }
public Clutter.Stage stage { get; protected set; }
public Clutter.Actor window_group { get; protected set; }
public Clutter.Actor top_window_group { get; protected set; }
public Meta.BackgroundGroup background_group { get; protected set; }

public virtual ModalProxy push_modal (Clutter.Actor actor, bool grab) {
return new ModalProxy ();
}

public virtual void pop_modal (ModalProxy proxy) {
}

public virtual bool is_modal () {
return false;
}

public virtual bool modal_proxy_valid (ModalProxy proxy) {
return true;
}

public virtual void perform_action (ActionType type) {
}

public virtual void move_window (Meta.Window? window, Meta.Workspace workspace, uint32 timestamp) {
}

public virtual void switch_to_next_workspace (Meta.MotionDirection direction, uint32 timestamp) {
}

public virtual void launch_action (string action_key) {
}

public virtual bool filter_action (GestureAction action) {
return false;
}
}
82 changes: 82 additions & 0 deletions tests/TestCase.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2025 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*/

/**
* A simple test case class. To use it inherit from it and add test methods
* in the constructor using {@link add_test}. Override {@link set_up} and {@link tear_down}
* to provide per-test-method setup and teardown. Then add a `main()` function
* and return the result of {@link run}.
*/
public abstract class Gala.TestCase : Object {
public delegate void TestMethod ();

public string name { get; construct; }

private GLib.TestSuite suite;
private Adaptor[] adaptors = new Adaptor[0];

construct {
suite = new GLib.TestSuite (name);
}

public int run (string[] args) {
Test.init (ref args);
TestSuite.get_root ().add_suite ((owned) suite);
return Test.run ();
}

protected void add_test (string name, owned TestMethod test) {
var adaptor = new Adaptor (name, (owned) test, this);
adaptors += adaptor;

var test_case = new GLib.TestCase (
adaptor.name,
adaptor.set_up,
adaptor.run,
adaptor.tear_down
);

suite.add ((owned) test_case);
}

public virtual void set_up () {
}

public virtual void tear_down () {
}

public void assert_finalize_object<G> (ref G data) {
unowned var weak_pointer = data;
((Object) data).add_weak_pointer (&weak_pointer);
data = null;
assert_null (weak_pointer);
}

private class Adaptor : Object {
public string name { get; construct; }

private TestMethod test;
private TestCase test_case;

public Adaptor (string name, owned TestMethod test, TestCase test_case) {
Object (name: name);

this.test = (owned) test;
this.test_case = test_case;
}

public void set_up (void* fixture) {
test_case.set_up ();
}

public void run (void* fixture) {
test ();
}

public void tear_down (void* fixture) {
test_case.tear_down ();
}
}
}
165 changes: 165 additions & 0 deletions tests/lib/GestureControllerTest.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright 2025 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*/

public class Gala.MockRootTarget : Object, GestureTarget, RootTarget {
public struct ReceivedUpdate {
public UpdateType type;
public GestureAction action;
public double progress;
}

public Clutter.Actor? actor { get { return null; } }

public Gee.LinkedList<ReceivedUpdate?> received_updates { get; construct; }

construct {
received_updates = new Gee.LinkedList<ReceivedUpdate?> ();
}

public override void propagate (UpdateType update_type, GestureAction action, double progress) {
received_updates.add (ReceivedUpdate () {
type = update_type,
action = action,
progress = progress
});
}
}

internal class Gala.MockGestureBackend : Object, GestureBackend {
}

public class Gala.GestureControllerTest : TestCase {
private Settings gala_settings;

private MockGestureBackend? backend;
private GestureController? controller;
private MockRootTarget? root_target;

public GestureControllerTest () {
Object (name: "GestureController");
}

construct {
new GestureSettings (); // Ensure static variables are initialized

// Make sure we configure swipe up to trigger multitasking view
gala_settings = new GLib.Settings ("io.elementary.desktop.wm.gestures");
gala_settings.delay ();
gala_settings.set_string ("three-finger-swipe-up", "multitasking-view");

add_test ("initial update", test_initial_update);
add_test ("simple propagation", test_simple_propagation);
}

public override void set_up () {
backend = new MockGestureBackend ();

controller = new GestureController (MULTITASKING_VIEW, new MockWindowManager ());
controller.enable_backend (backend);

root_target = new MockRootTarget ();
root_target.add_gesture_controller (controller);
}

public override void tear_down () {
backend = null;
controller = null;
root_target = null;
}

private void test_initial_update () {
// Test that we got the initial update when the controller was attached
assert_cmpint (root_target.received_updates.size, EQ, 1);

var sync_update = root_target.received_updates.poll ();

assert_true (sync_update != null);
assert_cmpint (sync_update.type, EQ, GestureTarget.UpdateType.UPDATE);
assert_cmpint (sync_update.action, EQ, GestureAction.MULTITASKING_VIEW);
assert_cmpfloat (sync_update.progress, EQ, 0.0);

assert_true (root_target.received_updates.is_empty);
}

private void test_simple_propagation () {
root_target.received_updates.poll (); // Remove the initial update

// Test a simple propagation flow
var gesture = new Gesture () {
type = TOUCHPAD_SWIPE,
direction = UP,
fingers = 3,
};

var recognizing = backend.on_gesture_detected (gesture, 0);

assert_true (recognizing);
assert_true (root_target.received_updates.is_empty);
assert_true (controller.recognizing);

backend.on_begin (0, 0);

assert_true (root_target.received_updates.size == 1);

var update = root_target.received_updates.poll ();
assert_true (update != null);
assert_true (update.type == START);
assert_true (update.action == MULTITASKING_VIEW);
assert_true (update.progress == 0.0);

backend.on_update (0.25, 0);
backend.on_update (0.5, 0);

assert_cmpfloat (controller.progress, EQ, 0.5);

backend.on_update (0.75, 0);
backend.on_update (1.0, 0);

assert_true (root_target.received_updates.size == 4);

for (int i = 0; i < 4; i++) {
update = root_target.received_updates.poll ();
assert_true (update != null);
assert_true (update.type == UPDATE);
assert_true (update.action == MULTITASKING_VIEW);
assert_cmpfloat (update.progress, EQ, (i + 1) * 0.25);
}

backend.on_end (1.0, 0);

assert_true (root_target.received_updates.size == 3);

update = root_target.received_updates.poll ();
assert_true (update != null);
assert_true (update.type == UPDATE);
assert_true (update.action == MULTITASKING_VIEW);
assert_cmpfloat (update.progress, EQ, 1.0);

update = root_target.received_updates.poll ();
assert_true (update != null);
assert_true (update.type == COMMIT);
assert_true (update.action == MULTITASKING_VIEW);
assert_cmpfloat (update.progress, EQ, 1.0);

update = root_target.received_updates.poll ();
assert_true (update != null);
assert_true (update.type == END);
assert_true (update.action == MULTITASKING_VIEW);
assert_cmpfloat (update.progress, EQ, 1.0);

assert_true (root_target.received_updates.is_empty);

assert_false (controller.recognizing);
assert_cmpfloat (controller.progress, EQ, 1.0);

assert_finalize_object<MockRootTarget> (ref root_target);
assert_finalize_object<GestureController> (ref controller);
assert_finalize_object<MockGestureBackend> (ref backend);
}
}

public int main (string[] args) {
return new Gala.GestureControllerTest ().run (args);
}
Loading
Loading