This makes our tests less flaky, shorter, and more readable.
fixes #2988
use constant TYPE_GET_VERSION => 7;
use constant TYPE_GET_BINDING_MODES => 8;
use constant TYPE_GET_CONFIG => 9;
+use constant TYPE_SEND_TICK => 10;
our %EXPORT_TAGS = ( 'all' => [
qw(i3 TYPE_RUN_COMMAND TYPE_COMMAND TYPE_GET_WORKSPACES TYPE_SUBSCRIBE TYPE_GET_OUTPUTS
TYPE_GET_TREE TYPE_GET_MARKS TYPE_GET_BAR_CONFIG TYPE_GET_VERSION
- TYPE_GET_BINDING_MODES TYPE_GET_CONFIG)
+ TYPE_GET_BINDING_MODES TYPE_GET_CONFIG TYPE_SEND_TICK)
] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
barconfig_update => ($event_mask | 4),
binding => ($event_mask | 5),
shutdown => ($event_mask | 6),
+ tick => ($event_mask | 7),
_error => 0xFFFFFFFF,
);
$self->message(TYPE_GET_CONFIG);
}
+=head2 send_tick
+
+Sends a tick event. Requires i3 >= 4.15
+
+=cut
+sub send_tick {
+ my ($self, $payload) = @_;
+
+ $self->_ensure_connection;
+
+ $self->message(TYPE_SEND_TICK, $payload);
+}
=head2 command($content)
| 7 | +GET_VERSION+ | <<_version_reply,VERSION>> | Gets the i3 version.
| 8 | +GET_BINDING_MODES+ | <<_binding_modes_reply,BINDING_MODES>> | Gets the names of all currently configured binding modes.
| 9 | +GET_CONFIG+ | <<_config_reply,CONFIG>> | Returns the last loaded i3 config.
+| 10 | +SEND_TICK+ | <<_tick_reply,TICK>> | Sends a tick event with the specified payload.
|======================================================
So, a typical message could look like this:
Reply to the GET_BINDING_MODES message.
GET_CONFIG (9)::
Reply to the GET_CONFIG message.
+TICK (10)::
+ Reply to the SEND_TICK message.
[[_command_reply]]
=== COMMAND reply
{ "config": "font pango:monospace 8\nbindsym Mod4+q exit\n" }
-------------------
+[[_tick_reply]]
+=== TICK reply
+
+The reply is a map containing the "success" member. After the reply was
+received, the tick event has been written to all IPC connections which subscribe
+to tick events. UNIX sockets are usually buffered, but you can be certain that
+once you receive the tick event you just triggered, you must have received all
+events generated prior to the +SEND_TICK+ message (happened-before relation).
+
+*Example:*
+-------------------
+{ "success": true }
+-------------------
== Events
mouse
shutdown (6)::
Sent when the ipc shuts down because of a restart or exit by user command
+tick (7)::
+ Sent when the ipc client subscribes to the tick event (with +"first":
+ true+) or when any ipc client sends a SEND_TICK message (with +"first":
+ false+).
*Example:*
--------------------------------------------------------------------
}
---------------------------
+=== tick event
+
+This event is triggered by a subscription to tick events or by a +SEND_TICK+
+message.
+
+*Example (upon subscription):*
+--------------------------------------------------------------------------------
+{
+ "first": true,
+ "payload": ""
+}
+--------------------------------------------------------------------------------
+
+*Example (upon +SEND_TICK+ with a payload of +arbitrary string+):*
+--------------------------------------------------------------------------------
+{
+ "first": false,
+ "payload": "arbitrary string"
+}
+--------------------------------------------------------------------------------
+
== See also (existing libraries)
[[libraries]]
message_type = I3_IPC_MESSAGE_TYPE_GET_VERSION;
} else if (strcasecmp(optarg, "get_config") == 0) {
message_type = I3_IPC_MESSAGE_TYPE_GET_CONFIG;
+ } else if (strcasecmp(optarg, "send_tick") == 0) {
+ message_type = I3_IPC_MESSAGE_TYPE_SEND_TICK;
} else {
printf("Unknown message type\n");
- printf("Known types: run_command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version, get_config\n");
+ printf("Known types: run_command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_binding_modes, get_version, get_config, send_tick\n");
exit(EXIT_FAILURE);
}
} else if (o == 'q') {
/** Request the raw last loaded i3 config. */
#define I3_IPC_MESSAGE_TYPE_GET_CONFIG 9
+/** Send a tick event to all subscribers. */
+#define I3_IPC_MESSAGE_TYPE_SEND_TICK 10
+
/*
* Messages from i3 to clients
*
#define I3_IPC_REPLY_TYPE_VERSION 7
#define I3_IPC_REPLY_TYPE_BINDING_MODES 8
#define I3_IPC_REPLY_TYPE_CONFIG 9
+#define I3_IPC_REPLY_TYPE_TICK 10
/*
* Events from i3 to clients. Events have the first bit set high.
/** The shutdown event will be triggered when the ipc shuts down */
#define I3_IPC_EVENT_SHUTDOWN (I3_IPC_EVENT_MASK | 6)
+
+/** The tick event will be sent upon a tick IPC message */
+#define I3_IPC_EVENT_TICK (I3_IPC_EVENT_MASK | 7)
int num_events;
char **events;
+ /* For clients which subscribe to the tick event: whether the first tick
+ * event has been sent by i3. */
+ bool first_tick_sent;
+
TAILQ_ENTRY(ipc_client)
clients;
} ipc_client;
memcpy(client->events[event], s, len);
DLOG("client is now subscribed to:\n");
- for (int i = 0; i < client->num_events; i++)
+ for (int i = 0; i < client->num_events; i++) {
DLOG("event %s\n", client->events[i]);
+ }
DLOG("(done)\n");
return 1;
yajl_free(p);
const char *reply = "{\"success\":true}";
ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_SUBSCRIBE, (const uint8_t *)reply);
+
+ if (client->first_tick_sent) {
+ return;
+ }
+
+ bool is_tick = false;
+ for (int i = 0; i < client->num_events; i++) {
+ if (strcmp(client->events[i], "tick") == 0) {
+ is_tick = true;
+ break;
+ }
+ }
+ if (!is_tick) {
+ return;
+ }
+
+ client->first_tick_sent = true;
+ const char *payload = "{\"first\":true,\"payload\":\"\"}";
+ ipc_send_message(client->fd, strlen(payload), I3_IPC_EVENT_TICK, (const uint8_t *)payload);
}
/*
y(free);
}
+/*
+ * Sends the tick event from the message payload to subscribers. Establishes a
+ * synchronization point in event-related tests.
+ */
+IPC_HANDLER(send_tick) {
+ yajl_gen gen = ygenalloc();
+
+ y(map_open);
+
+ ystr("payload");
+ yajl_gen_string(gen, (unsigned char *)message, message_size);
+
+ y(map_close);
+
+ const unsigned char *payload;
+ ylength length;
+ y(get_buf, &payload, &length);
+
+ ipc_send_event("tick", I3_IPC_EVENT_TICK, (const char *)payload);
+ y(free);
+
+ const char *reply = "{\"success\":true}";
+ ipc_send_message(fd, strlen(reply), I3_IPC_REPLY_TYPE_TICK, (const uint8_t *)reply);
+ DLOG("Sent tick event\n");
+}
+
/* The index of each callback function corresponds to the numeric
* value of the message type (see include/i3/ipc.h) */
-handler_t handlers[10] = {
+handler_t handlers[11] = {
handle_run_command,
handle_get_workspaces,
handle_subscribe,
handle_get_version,
handle_get_binding_modes,
handle_get_config,
+ handle_send_tick,
};
/*
wait_for_unmap
$x
kill_all_windows
+ events_for
+ listen_for_binding
);
=head1 NAME
cmd '[title=".*"] kill';
}
+=head2 events_for($subscribecb, [ $rettype ], [ $eventcbs ])
+
+Helper function which returns an array containing all events of type $rettype
+which were generated by i3 while $subscribecb was running.
+
+Set $eventcbs to subscribe to multiple event types and/or perform your own event
+aggregation.
+
+=cut
+sub events_for {
+ my ($subscribecb, $rettype, $eventcbs) = @_;
+
+ my @events;
+ $eventcbs //= {};
+ if (defined($rettype)) {
+ $eventcbs->{$rettype} = sub { push @events, shift };
+ }
+ my $subscribed = AnyEvent->condvar;
+ my $flushed = AnyEvent->condvar;
+ $eventcbs->{tick} = sub {
+ my ($event) = @_;
+ if ($event->{first}) {
+ $subscribed->send($event);
+ } else {
+ $flushed->send($event);
+ }
+ };
+ my $i3 = i3(get_socket_path(0));
+ $i3->connect->recv;
+ $i3->subscribe($eventcbs)->recv;
+ $subscribed->recv;
+ # Subscription established, run the callback.
+ $subscribecb->();
+ # Now generate a tick event, which we know we’ll receive (and at which point
+ # all other events have been received).
+ my $nonce = int(rand(255)) + 1;
+ $i3->send_tick($nonce);
+
+ my $tick = $flushed->recv;
+ $tester->is_eq($tick->{payload}, $nonce, 'tick nonce received');
+ return @events;
+}
+
+=head2 listen_for_binding($cb)
+
+Helper function to evaluate whether sending KeyPress/KeyRelease events via XTEST
+triggers an i3 key binding or not. Expects key bindings to be configured in the
+form “bindsym <binding> nop <binding>”, e.g. “bindsym Mod4+Return nop
+Mod4+Return”.
+
+ is(listen_for_binding(
+ sub {
+ xtest_key_press(133); # Super_L
+ xtest_key_press(36); # Return
+ xtest_key_release(36); # Return
+ xtest_key_release(133); # Super_L
+ xtest_sync_with_i3;
+ },
+ ),
+ 'Mod4+Return',
+ 'triggered the "Mod4+Return" keybinding');
+
+=cut
+
+sub listen_for_binding {
+ my ($cb) = @_;
+ my $triggered = AnyEvent->condvar;
+ my @events = events_for(
+ $cb,
+ 'binding');
+
+ $tester->is_eq(scalar @events, 1, 'Received precisely one event');
+ $tester->is_eq($events[0]->{change}, 'run', 'change is "run"');
+ # We look at the command (which is “nop <binding>”) because that is easier
+ # than re-assembling the string representation of $event->{binding}.
+ my $command = $events[0]->{binding}->{command};
+ $command =~ s/^nop //g;
+ return $command;
+}
+
=head1 AUTHOR
Michael Stapelberg <michael@i3wm.org>
xtest_key_release
xtest_button_press
xtest_button_release
- listen_for_binding
- start_binding_capture
binding_events
);
=cut
-my $i3;
-our @binding_events;
-
-=head2 start_binding_capture()
-
-Captures all binding events sent by i3 in the C<@binding_events> symbol, so
-that you can verify the correct number of binding events was generated.
-
- my $pid = launch_with_config($config);
- start_binding_capture;
- # …
- sync_with_i3;
- is(scalar @i3test::XTEST::binding_events, 2, 'Received exactly 2 binding events');
-
-=cut
-
-sub start_binding_capture {
- # Store a copy of each binding event so that we can count the expected
- # events in test cases.
- $i3 = i3(get_socket_path());
- $i3->connect()->recv;
- $i3->subscribe({
- binding => sub {
- my ($event) = @_;
- @binding_events = (@binding_events, $event);
- },
- })->recv;
-}
-
-=head2 listen_for_binding($cb)
-
-Helper function to evaluate whether sending KeyPress/KeyRelease events via
-XTEST triggers an i3 key binding or not (with a timeout of 0.5s). Expects key
-bindings to be configured in the form “bindsym <binding> nop <binding>”, e.g.
-“bindsym Mod4+Return nop Mod4+Return”.
-
- is(listen_for_binding(
- sub {
- xtest_key_press(133); # Super_L
- xtest_key_press(36); # Return
- xtest_key_release(36); # Return
- xtest_key_release(133); # Super_L
- },
- ),
- 'Mod4+Return',
- 'triggered the "Mod4+Return" keybinding');
-
-=cut
-
-sub listen_for_binding {
- my ($cb) = @_;
- my $triggered = AnyEvent->condvar;
- my $i3 = i3(get_socket_path());
- $i3->connect()->recv;
- $i3->subscribe({
- binding => sub {
- my ($event) = @_;
- return unless $event->{change} eq 'run';
- # We look at the command (which is “nop <binding>”) because that is
- # easier than re-assembling the string representation of
- # $event->{binding}.
- $triggered->send($event->{binding}->{command});
- },
- })->recv;
-
- my $t;
- $t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $triggered->send('timeout');
- }
- );
-
- $cb->();
-
- my $recv = $triggered->recv;
- $recv =~ s/^nop //g;
- return $recv;
-}
-
=head2 set_xkb_group($group)
Changes the current XKB group from the default of 1 to C<$group>, which must be
use i3test;
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-################################
-# Workspaces requests and events
-################################
-
my $old_ws = get_ws(focused_ws());
-# Events
-
# We are switching to an empty workpspace from an empty workspace, so we expect
# to receive "init", "focus", and "empty".
-my $init = AnyEvent->condvar;
-my $focus = AnyEvent->condvar;
-my $empty = AnyEvent->condvar;
-$i3->subscribe({
- workspace => sub {
- my ($event) = @_;
- if ($event->{change} eq 'init') {
- $init->send($event);
- } elsif ($event->{change} eq 'focus') {
- $focus->send($event);
- } elsif ($event->{change} eq 'empty') {
- $empty->send($event);
- }
- }
-})->recv;
-
-cmd 'workspace 2';
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $init->send(0);
- $focus->send(0);
- $empty->send(0);
- }
-);
-
-my $init_event = $init->recv;
-my $focus_event = $focus->recv;
-my $empty_event = $empty->recv;
+my @events = events_for(
+ sub { cmd 'workspace 2' },
+ 'workspace');
my $current_ws = get_ws(focused_ws());
-ok($init_event, 'workspace "init" event received');
-is($init_event->{current}->{id}, $current_ws->{id}, 'the "current" property should contain the initted workspace con');
+is(scalar @events, 3, 'Received 3 events');
+is($events[0]->{change}, 'init', 'First event has change = init');
+is($events[0]->{current}->{id}, $current_ws->{id}, 'the "current" property contains the initted workspace con');
-ok($focus_event, 'workspace "focus" event received');
-is($focus_event->{current}->{id}, $current_ws->{id}, 'the "current" property should contain the focused workspace con');
-is($focus_event->{old}->{id}, $old_ws->{id}, 'the "old" property should contain the workspace con that was focused last');
+is($events[1]->{change}, 'focus', 'Second event has change = focus');
+is($events[1]->{current}->{id}, $current_ws->{id}, 'the "current" property should contain the focused workspace con');
+is($events[1]->{old}->{id}, $old_ws->{id}, 'the "old" property should contain the workspace con that was focused last');
-ok($empty_event, 'workspace "empty" event received');
-is($empty_event->{current}->{id}, $old_ws->{id}, 'the "current" property should contain the emptied workspace con');
+is($events[2]->{change}, 'empty', 'Third event has change = empty');
+is($events[2]->{current}->{id}, $old_ws->{id}, 'the "current" property should contain the emptied workspace con');
done_testing;
}
EOT
-my $i3 = i3(get_socket_path(0));
-$i3->connect->recv;
+my @events = events_for(
+ sub { cmd 'mode "m1"' },
+ 'mode');
-my $cv = AnyEvent->condvar;
-
-$i3->subscribe({
- mode => sub {
- my ($event) = @_;
- $cv->send($event->{change} eq 'm1');
- }
-})->recv;
-
-cmd 'mode "m1"';
-
-# Timeout after 0.5s
-my $t;
-$t = AnyEvent->timer(after => 0.5, cb => sub { $cv->send(0); });
-
-ok($cv->recv, 'Mode event received');
+my @changes = map { $_->{change} } @events;
+is_deeply(\@changes, [ 'm1' ], 'Mode event received');
done_testing;
use i3test;
-SKIP: {
-
- skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
-
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-################################
-# Window event
-################################
-
-# Events
-
my $new = AnyEvent->condvar;
my $focus = AnyEvent->condvar;
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- if ($event->{change} eq 'new') {
- $new->send($event);
- } elsif ($event->{change} eq 'focus') {
- $focus->send($event);
- }
- }
-})->recv;
-
-open_window;
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $new->send(0);
- $focus->send(0);
- }
-);
-is($new->recv->{container}->{focused}, 0, 'Window "new" event received');
-is($focus->recv->{container}->{focused}, 1, 'Window "focus" event received');
+my @events = events_for(
+ sub { open_window },
+ 'window');
-}
+is(scalar @events, 2, 'Received 2 events');
+is($events[0]->{container}->{focused}, 0, 'Window "new" event received');
+is($events[1]->{container}->{focused}, 1, 'Window "focus" event received');
done_testing;
use i3test;
-SKIP: {
-
- skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
-
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
################################
# Window focus event
################################
my $win1 = open_window;
my $win2 = open_window;
-my $focus = AnyEvent->condvar;
-
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- $focus->send($event);
- }
-})->recv;
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $focus->send(0);
- }
-);
-
# ensure the rightmost window contains input focus
-$i3->command('[id="' . $win2->id . '"] focus')->recv;
+cmd '[id="' . $win2->id . '"] focus';
is($x->input_focus, $win2->id, "Window 2 focused");
-cmd 'focus left';
-my $event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($focus->recv->{container}->{name}, 'Window 1', 'Window 1 focused');
-
-$focus = AnyEvent->condvar;
-cmd 'focus left';
-$event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($event->{container}->{name}, 'Window 0', 'Window 0 focused');
-
-$focus = AnyEvent->condvar;
-cmd 'focus right';
-$event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($event->{container}->{name}, 'Window 1', 'Window 1 focused');
+sub focus_subtest {
+ my ($cmd, $name) = @_;
-$focus = AnyEvent->condvar;
-cmd 'focus right';
-$event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($event->{container}->{name}, 'Window 2', 'Window 2 focused');
+ my $focus = AnyEvent->condvar;
-$focus = AnyEvent->condvar;
-cmd 'focus right';
-$event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($event->{container}->{name}, 'Window 0', 'Window 0 focused');
-
-$focus = AnyEvent->condvar;
-cmd 'focus left';
-$event = $focus->recv;
-is($event->{change}, 'focus', 'Focus event received');
-is($event->{container}->{name}, 'Window 2', 'Window 2 focused');
+ my @events = events_for(
+ sub { cmd $cmd },
+ 'window');
+ is(scalar @events, 1, 'Received 1 event');
+ is($events[0]->{change}, 'focus', 'Focus event received');
+ is($events[0]->{container}->{name}, $name, "$name focused");
}
+subtest 'focus left (1)', \&focus_subtest, 'focus left', 'Window 1';
+subtest 'focus left (2)', \&focus_subtest, 'focus left', 'Window 0';
+subtest 'focus right (1)', \&focus_subtest, 'focus right', 'Window 1';
+subtest 'focus right (2)', \&focus_subtest, 'focus right', 'Window 2';
+subtest 'focus right (3)', \&focus_subtest, 'focus right', 'Window 0';
+subtest 'focus left', \&focus_subtest, 'focus left', 'Window 2';
+
done_testing;
use i3test;
-SKIP: {
-
- skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
-
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-################################
-# Window title event
-################################
-
my $window = open_window(name => 'Window 0');
-my $title = AnyEvent->condvar;
-
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- $title->send($event);
- }
-})->recv;
-
-$window->name('New Window Title');
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $title->send(0);
- }
-);
-
-my $event = $title->recv;
-is($event->{change}, 'title', 'Window title change event received');
-is($event->{container}->{name}, 'New Window Title', 'Window title changed');
+my @events = events_for(
+ sub {
+ $window->name('New Window Title');
+ sync_with_i3;
+ },
+ 'window');
-}
+is(scalar @events, 1, 'Received 1 event');
+is($events[0]->{change}, 'title', 'Window title change event received');
+is($events[0]->{container}->{name}, 'New Window Title', 'Window title changed');
done_testing;
# Bug still in: 4.7.2-135-g7deb23c
use i3test;
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
+open_window;
-my $cv;
-my $t;
+sub fullscreen_subtest {
+ my ($want) = @_;
+ my @events = events_for(
+ sub { cmd 'fullscreen' },
+ 'window');
-sub reset_test {
- $cv = AE::cv;
- $t = AE::timer(0.5, 0, sub { $cv->send(0); });
+ is(scalar @events, 1, 'Received 1 event');
+ is($events[0]->{container}->{fullscreen_mode}, $want, "fullscreen_mode now $want");
}
-reset_test;
-
-$i3->subscribe({
- window => sub {
- my ($e) = @_;
- if ($e->{change} eq 'fullscreen_mode') {
- $cv->send($e->{container});
- }
- },
- })->recv;
-
-my $window = open_window;
-
-cmd 'fullscreen';
-my $con = $cv->recv;
-
-ok($con, 'got fullscreen window event (on)');
-is($con->{fullscreen_mode}, 1, 'window is fullscreen');
-
-reset_test;
-cmd 'fullscreen';
-$con = $cv->recv;
-
-ok($con, 'got fullscreen window event (off)');
-is($con->{fullscreen_mode}, 0, 'window is not fullscreen');
+subtest 'fullscreen on', \&fullscreen_subtest, 1;
+subtest 'fullscreen off', \&fullscreen_subtest, 0;
done_testing;
#
use i3test;
-SKIP: {
-
- skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
-
################################################################################
-# check that the workspace empty event is send upon workspace switch when the
+# check that the workspace empty event is sent upon workspace switch when the
# old workspace is empty
################################################################################
subtest 'Workspace empty event upon switch', sub {
cmd '[id="' . $w1->id . '"] kill';
my $cond = AnyEvent->condvar;
- my $client = i3(get_socket_path(0));
- $client->connect()->recv;
- $client->subscribe({
- workspace => sub {
- my ($event) = @_;
- $cond->send($event);
- }
- })->recv;
-
- cmd "workspace $ws2";
-
- sync_with_i3;
+ my @events = events_for(
+ sub { cmd "workspace $ws2" },
+ 'workspace');
- my $event = $cond->recv;
- is($event->{change}, 'empty', '"Empty" event received upon workspace switch');
- is($event->{current}->{name}, $ws1, '"current" property should be set to the workspace con');
+ is(scalar @events, 2, 'Received 2 event');
+ is($events[1]->{change}, 'empty', '"Empty" event received upon workspace switch');
+ is($events[1]->{current}->{name}, $ws1, '"current" property should be set to the workspace con');
};
################################################################################
-# check that no workspace empty event is send upon workspace switch if the
+# check that no workspace empty event is sent upon workspace switch if the
# workspace is not empty
################################################################################
subtest 'No workspace empty event', sub {
my $ws1 = fresh_workspace;
my $w1 = open_window();
- my @events;
- my $cond = AnyEvent->condvar;
- my $client = i3(get_socket_path(0));
- $client->connect()->recv;
- $client->subscribe({
- workspace => sub {
- my ($event) = @_;
- push @events, $event;
- }
- })->recv;
-
- # Wait for the workspace event on a new connection. Events will be delivered
- # to older connections earlier, so by the time it arrives here, it should be
- # in @events already.
- my $ws_event_block_conn = i3(get_socket_path(0));
- $ws_event_block_conn->connect()->recv;
- $ws_event_block_conn->subscribe({ workspace => sub { $cond->send(1) }});
-
- cmd "workspace $ws2";
+ my @events = events_for(
+ sub { cmd "workspace $ws2" },
+ 'workspace');
- sync_with_i3;
-
- my @expected_events = grep { $_->{change} eq 'focus' } @events;
- my @empty_events = grep { $_->{change} eq 'empty' } @events;
- is(@expected_events, 1, '"Focus" event received');
- is(@empty_events, 0, 'No "empty" events received');
+ is(scalar @events, 1, 'Received 1 event');
+ is($events[0]->{change}, 'focus', 'Event change is "focus"');
};
################################################################################
-# check that workspace empty event is send when the last window has been closed
+# check that workspace empty event is sent when the last window has been closed
# on invisible workspace
################################################################################
subtest 'Workspace empty event upon window close', sub {
my $ws2 = fresh_workspace;
my $w2 = open_window();
- my $cond = AnyEvent->condvar;
- my $client = i3(get_socket_path(0));
- $client->connect()->recv;
- $client->subscribe({
- workspace => sub {
- my ($event) = @_;
- $cond->send($event);
- }
- })->recv;
-
- cmd '[id="' . $w1->id . '"] kill';
-
- sync_with_i3;
+ my @events = events_for(
+ sub {
+ $w1->unmap;
+ sync_with_i3;
+ },
+ 'workspace');
- my $event = $cond->recv;
- is($event->{change}, 'empty', '"Empty" event received upon window close');
- is($event->{current}->{name}, $ws1, '"current" property should be set to the workspace con');
+ is(scalar @events, 1, 'Received 1 event');
+ is($events[0]->{change}, 'empty', '"Empty" event received upon window close');
+ is($events[0]->{current}->{name}, $ws1, '"current" property should be set to the workspace con');
};
-}
-
done_testing;
# Bug still in: 4.8-7-gf4a8253
use i3test;
-my $i3 = i3(get_socket_path());
-$i3->connect->recv;
+sub floating_subtest {
+ my ($win, $cmd, $want) = @_;
-my $cv = AnyEvent->condvar;
+ my @events = events_for(
+ sub { cmd $cmd },
+ 'window');
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- $cv->send($event) if $event->{change} eq 'floating';
- }
- })->recv;
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $cv->send(0);
- }
-);
+ my @floating = grep { $_->{change} eq 'floating' } @events;
+ is(scalar @floating, 1, 'Received 1 floating event');
+ is($floating[0]->{container}->{window}, $win->{id}, "window id matches");
+ is($floating[0]->{container}->{floating}, $want, "floating is $want");
+}
my $win = open_window();
-cmd '[id="' . $win->{id} . '"] floating enable';
-my $e = $cv->recv;
-
-isnt($e, 0, 'floating a container should send an ipc window event');
-is($e->{container}->{window}, $win->{id}, 'the event should contain information about the window');
-is($e->{container}->{floating}, 'user_on', 'the container should be floating');
-
-$cv = AnyEvent->condvar;
-cmd '[id="' . $win->{id} . '"] floating disable';
-$e = $cv->recv;
-
-isnt($e, 0, 'disabling floating on a container should send an ipc window event');
-is($e->{container}->{window}, $win->{id}, 'the event should contain information about the window');
-is($e->{container}->{floating}, 'user_off', 'the container should not be floating');
+subtest 'floating enable', \&floating_subtest, $win, '[id="' . $win->{id} . '"] floating enable', 'user_on';
+subtest 'floating disable', \&floating_subtest, $win, '[id="' . $win->{id} . '"] floating disable', 'user_off';
done_testing;
skip 'xdotool is required to test the binding event. `[apt-get install|pacman -S] xdotool`', 1 if $?;
- skip "AnyEvent::I3 too old (need >= 0.16)", 1 if $AnyEvent::I3::VERSION < 0.16;
-
my $pid = launch_with_config($config);
- my $i3 = i3(get_socket_path());
- $i3->connect->recv;
-
- my $cv = AE::cv;
- my $timer = AE::timer 0.5, 0, sub { $cv->send(0); };
-
- $i3->subscribe({
- binding => sub {
- $cv->send(shift);
- }
- })->recv;
-
- qx(xdotool key $binding_symbol);
-
- my $e = $cv->recv;
-
- does_i3_live;
+ my $cv = AnyEvent->condvar;
- diag "Event:\n", Dumper($e);
+ my @events = events_for(
+ sub {
+ # TODO: this is still flaky: we need to synchronize every X11
+ # connection with i3. Move to XTEST and synchronize that connection.
+ qx(xdotool key $binding_symbol);
+ },
+ 'binding');
- ok($e,
- 'the binding event should emit when user input triggers an i3 binding event');
+ is(scalar @events, 1, 'Received 1 event');
- is($e->{change}, 'run',
+ is($events[0]->{change}, 'run',
'the `change` field should indicate this binding has run');
- ok($e->{binding},
+ ok($events[0]->{binding},
'the `binding` field should be a hash that contains information about the binding');
- is($e->{binding}->{input_type}, 'keyboard',
+ is($events[0]->{binding}->{input_type}, 'keyboard',
'the input_type field should be the input type of the binding (keyboard or mouse)');
note 'the `mods` field should contain the symbols for the modifiers of the binding';
foreach (@mods) {
- ok(grep(/$_/i, @{$e->{binding}->{mods}}), "`mods` contains the modifier $_");
+ ok(grep(/$_/i, @{$events[0]->{binding}->{mods}}), "`mods` contains the modifier $_");
}
- is($e->{binding}->{command}, $command,
+ is($events[0]->{binding}->{command}, $command,
'the `command` field should contain the command the binding ran');
- is($e->{binding}->{input_code}, 0,
+ is($events[0]->{binding}->{input_code}, 0,
'the input_code should be the specified code if the key was bound with bindcode, and otherwise zero');
exit_gracefully($pid);
skip "setxkbmap not found", 1 if
system(q|setxkbmap -print >/dev/null|) != 0;
-start_binding_capture;
-
system(q|setxkbmap us,ru -option grp:alt_shift_toggle|);
is(listen_for_binding(
'Mod4+Return',
'triggered the "Mod4+Return" keybinding');
-sync_with_i3;
-is(scalar @i3test::XTEST::binding_events, 4, 'Received exactly 4 binding events');
-
# Disable the grp:alt_shift_toggle option, as we use Alt+Shift in other testcases.
system(q|setxkbmap us -option|);
skip "libxcb-xkb too old (need >= 1.11)", 1 unless
ExtUtils::PkgConfig->atleast_version('xcb-xkb', '1.11');
-start_binding_capture;
-
is(listen_for_binding(
sub {
xtest_key_press(107); # Print
'Mod1+Shift+b release',
'triggered the "Mod1+Shift+b" release keybinding');
-sync_with_i3;
-is(scalar @i3test::XTEST::binding_events, 4, 'Received exactly 4 binding events');
-
}
done_testing;
cmd 'mode othermode';
-my $i3 = i3(get_socket_path(0));
-$i3->connect->recv;
+my @events = events_for(
+ sub { cmd 'reload' },
+ 'mode');
-my $cv = AnyEvent->condvar;
-$i3->subscribe({
- mode => sub {
- my ($event) = @_;
- $cv->send($event->{change} eq 'default');
- }
-})->recv;
-
-cmd 'reload';
-
-# Timeout after 0.5s
-my $t;
-$t = AnyEvent->timer(after => 0.5, cb => sub { $cv->send(0); });
-
-ok($cv->recv, 'Mode event received');
+is(scalar @events, 1, 'Received 1 event');
+is($events[0]->{change}, 'default', 'change is "default"');
done_testing;
# Ticket: #2501
use i3test;
-my ($i3, $timer, $event, $mark);
+sub mark_subtest {
+ my ($cmd) = @_;
-$i3 = i3(get_socket_path());
-$i3->connect()->recv;
+ my @events = events_for(
+ sub { cmd $cmd },
+ 'window');
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- return unless defined $mark;
- return unless $event->{change} eq 'mark';
-
- $mark->send($event);
- }
-})->recv;
-
-$timer = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $mark->send(0);
- }
-);
+ my @mark = grep { $_->{change} eq 'mark' } @events;
+ is(scalar @mark, 1, 'Received 1 window::mark event');
+}
###############################################################################
# Marking a container triggers a 'mark' event.
fresh_workspace;
open_window;
-$mark = AnyEvent->condvar;
-cmd 'mark x';
-
-$event = $mark->recv;
-ok($event, 'window::mark event has been received');
+subtest 'mark', \&mark_subtest, 'mark x';
###############################################################################
# Unmarking a container triggers a 'mark' event.
open_window;
cmd 'mark x';
-$mark = AnyEvent->condvar;
-cmd 'unmark x';
-
-$event = $mark->recv;
-ok($event, 'window::mark event has been received');
+subtest 'unmark', \&mark_subtest, 'unmark x';
###############################################################################
# Bug still in: 4.8-7-gf4a8253
use i3test;
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-my $cv;
-my $t;
-
-sub reset_test {
- $cv = AE::cv;
- $t = AE::timer(0.5, 0, sub { $cv->send(0); });
-}
-
-reset_test;
-
-$i3->subscribe({
- window => sub {
- my ($e) = @_;
- if ($e->{change} eq 'close') {
- $cv->send($e->{container});
- }
- },
- })->recv;
-
my $window = open_window;
-cmd 'kill';
-my $con = $cv->recv;
+my @events = events_for(
+ sub {
+ $window->unmap;
+ sync_with_i3;
+ },
+ 'window');
-ok($con, 'closing a window should send the window::close event');
-is($con->{window}, $window->{id}, 'the event should contain information about the window');
+my @close = grep { $_->{change} eq 'close' } @events;
+is(scalar @close, 1, 'Received 1 window::close event');
+is($close[0]->{container}->{window}, $window->{id}, 'the event should contain information about the window');
done_testing;
# Bug still in: 4.8-7-gf4a8253
use i3test;
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-my $cv;
-my $t;
-
-sub reset_test {
- $cv = AE::cv;
- $t = AE::timer(0.5, 0, sub { $cv->send(0); });
-}
-
-reset_test;
-
-$i3->subscribe({
- window => sub {
- my ($e) = @_;
- if ($e->{change} eq 'move') {
- $cv->send($e->{container});
- }
- },
- })->recv;
-
my $dummy_window = open_window;
my $window = open_window;
-cmd 'move right';
-my $con = $cv->recv;
-
-ok($con, 'moving a window should emit the window::move event');
-is($con->{window}, $window->{id}, 'the event should contain info about the window');
+sub move_subtest {
+ my ($cmd) = @_;
+ my $cv = AnyEvent->condvar;
+ my @events = events_for(
+ sub { cmd $cmd },
+ 'window');
-reset_test;
-
-cmd 'move to workspace ws_new';
-$con = $cv->recv;
+ my @move = grep { $_->{change} eq 'move' } @events;
+ is(scalar @move, 1, 'Received 1 window::move event');
+ is($move[0]->{container}->{window}, $window->{id}, 'window id matches');
+}
-ok($con, 'moving a window to a different workspace should emit the window::move event');
-is($con->{window}, $window->{id}, 'the event should contain info about the window');
+subtest 'move right', \&move_subtest, 'move right';
+subtest 'move to workspace', \&move_subtest, 'move to workspace ws_new';
done_testing;
#
use i3test;
-my $config = <<EOT;
-# i3 config file (v4)
-font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
-
-force_display_urgency_hint 0ms
-EOT
-
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-my $cv;
-$i3->subscribe({
- window => sub {
- my ($event) = @_;
- $cv->send($event) if $event->{change} eq 'urgent';
- }
-})->recv;
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $cv->send(0);
- }
-);
-
-$cv = AnyEvent->condvar;
fresh_workspace;
my $win = open_window;
my $dummy_win = open_window;
-$win->add_hint('urgency');
-my $event = $cv->recv;
-
-isnt($event, 0, 'an urgent con should emit the window::urgent event');
-is($event->{container}->{window}, $win->{id}, 'the event should contain information about the window');
-is($event->{container}->{urgent}, 1, 'the container should be urgent');
-
-$cv = AnyEvent->condvar;
-$win->delete_hint('urgency');
-$event = $cv->recv;
-
-isnt($event, 0, 'an urgent con should emit the window::urgent event');
-is($event->{container}->{window}, $win->{id}, 'the event should contain information about the window');
-is($event->{container}->{urgent}, 0, 'the container should not be urgent');
+sub urgency_subtest {
+ my ($subscribecb, $win, $want) = @_;
+
+ my @events = events_for(
+ $subscribecb,
+ 'window');
+
+ my @urgent = grep { $_->{change} eq 'urgent' } @events;
+ is(scalar @urgent, 1, 'Received 1 window::urgent event');
+ is($urgent[0]->{container}->{window}, $win->{id}, "window id matches");
+ is($urgent[0]->{container}->{urgent}, $want, "urgent is $want");
+}
+
+subtest "urgency set", \&urgency_subtest,
+ sub {
+ $win->add_hint('urgency');
+ sync_with_i3;
+ },
+ $win,
+ 1;
+
+subtest "urgency unset", \&urgency_subtest,
+ sub {
+ $win->delete_hint('urgency');
+ sync_with_i3;
+ },
+ $win,
+ 0;
done_testing;
# Bug still in: 4.12-46-g2123888
use i3test;
-SKIP: {
- skip "AnyEvent::I3 too old (need >= 0.17)", 1 if $AnyEvent::I3::VERSION < 0.17;
+# We cannot use events_for in this test as we cannot send events after
+# issuing the restart/shutdown command.
my $i3 = i3(get_socket_path());
$i3->connect->recv;
-my $cv = AE::cv;
+my $cv = AnyEvent->condvar;
my $timer = AE::timer 0.5, 0, sub { $cv->send(0); };
$i3->subscribe({
$i3 = i3(get_socket_path());
$i3->connect->recv;
-$cv = AE::cv;
+$cv = AnyEvent->condvar;
$timer = AE::timer 0.5, 0, sub { $cv->send(0); };
$i3->subscribe({
diag "Event:\n", Dumper($e);
ok($e, 'the shutdown event should emit when the ipc is exited by command');
is($e->{change}, 'exit', 'the `change` field should tell the reason for the shutdown');
-}
done_testing;
my $pid = launch_with_config($config);
-start_binding_capture;
-
is(listen_for_binding(
sub {
xtest_key_press(87); # KP_End
's',
'triggered the "s" keybinding with Num_Lock');
-sync_with_i3;
-is(scalar @i3test::XTEST::binding_events, 12, 'Received exactly 12 binding events');
-
exit_gracefully($pid);
################################################################################
$pid = launch_with_config($config);
-start_binding_capture;
-
is(listen_for_binding(
sub {
xtest_key_press(133); # Super_L
'Return',
'triggered the "Return" keybinding with Num_Lock');
-sync_with_i3;
-is(scalar @i3test::XTEST::binding_events, 16, 'Received exactly 16 binding events');
-
exit_gracefully($pid);
################################################################################
$pid = launch_with_config($config);
-start_binding_capture;
-
is(listen_for_binding(
sub {
xtest_key_press(87); # KP_End
'KP_Down',
'triggered the "KP_Down" keybinding');
-is(listen_for_binding(
+my @unexpected = events_for(
sub {
xtest_key_press(77); # enable Num_Lock
xtest_key_release(77); # enable Num_Lock
xtest_key_release(77); # disable Num_Lock
xtest_sync_with_i3;
},
- ),
- 'timeout',
- 'Did not trigger the KP_End keybinding with KP_1');
+ 'binding');
+is(scalar @unexpected, 0, 'Did not trigger the KP_End keybinding with KP_1');
-is(listen_for_binding(
+my @unexpected2 = events_for(
sub {
xtest_key_press(77); # enable Num_Lock
xtest_key_release(77); # enable Num_Lock
xtest_key_release(77); # disable Num_Lock
xtest_sync_with_i3;
},
- ),
- 'timeout',
- 'Did not trigger the KP_Down keybinding with KP_2');
+ 'binding');
-# TODO: This test does not verify that i3 does _NOT_ grab keycode 87 with Mod2.
+is(scalar @unexpected2, 0, 'Did not trigger the KP_Down keybinding with KP_2');
-sync_with_i3;
-is(scalar @i3test::XTEST::binding_events, 18, 'Received exactly 18 binding events');
+# TODO: This test does not verify that i3 does _NOT_ grab keycode 87 with Mod2.
exit_gracefully($pid);
my $win = open_window;
-start_binding_capture;
-
is(listen_for_binding(
sub {
xtest_key_press(77); # enable Num_Lock
#
# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
# (unless you are already familiar with Perl)
-#
+#
# Ticket: #990
# Bug still in: 4.5.1-23-g82b5978
fake-outputs 1024x768+0+0,1024x768+1024+0
EOT
-my $i3 = i3(get_socket_path());
-
-$i3->connect()->recv;
-
-################################
-# Workspaces requests and events
-################################
-
my $old_ws = get_ws(focused_ws);
-# Events
-
-# We are switching to an empty workpspace on the output to the right from an empty workspace on the output on the left, so we expect
-# to receive "init", "focus", and "empty".
my $focus = AnyEvent->condvar;
-$i3->subscribe({
- workspace => sub {
- my ($event) = @_;
- if ($event->{change} eq 'focus') {
- $focus->send($event);
- }
- }
-})->recv;
-
-my $t;
-$t = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $focus->send(0);
- }
-);
-
-cmd 'focus output right';
-
-my $event = $focus->recv;
+my @events = events_for(
+ sub { cmd 'focus output right' },
+ 'workspace');
my $current_ws = get_ws(focused_ws);
-ok($event, 'Workspace "focus" event received');
-is($event->{current}->{id}, $current_ws->{id}, 'Event gave correct current workspace');
-is($event->{old}->{id}, $old_ws->{id}, 'Event gave correct old workspace');
+is(scalar @events, 1, 'Received 1 event');
+is($events[0]->{current}->{id}, $current_ws->{id}, 'Event gave correct current workspace');
+is($events[0]->{old}->{id}, $old_ws->{id}, 'Event gave correct old workspace');
done_testing;
workspace ws-right output fake-1
EOT
-my $i3 = i3(get_socket_path());
-$i3->connect()->recv;
-
-# subscribe to the 'focus' ipc event
-my $focus = AnyEvent->condvar;
-$i3->subscribe({
- workspace => sub {
- my ($event) = @_;
- if ($event->{change} eq 'focus') {
- $focus->send($event);
- }
- }
-})->recv;
-
-# give up after 0.5 seconds
-my $timer = AnyEvent->timer(
- after => 0.5,
- cb => sub {
- $focus->send(0);
- }
-);
-
# open two windows on the left output
cmd 'workspace ws-left';
open_window;
open_window;
-# move a window over to the right output
-cmd 'move right';
-my $event = $focus->recv;
+sub focus_subtest {
+ my ($cmd, $want) = @_;
-ok($event, 'moving from workspace with two windows triggered focus ipc event');
-is($event->{current}->{name}, 'ws-right', 'focus event gave the right workspace');
-is(@{$event->{current}->{nodes}}, 1, 'focus event gave the right number of windows on the workspace');
+ my @events = events_for(
+ sub { cmd $cmd },
+ 'workspace');
-# reset and try again
-$focus = AnyEvent->condvar;
-cmd 'workspace ws-left';
-$focus->recv;
+ my @focus = grep { $_->{change} eq 'focus' } @events;
+ is(scalar @focus, 1, 'Received 1 workspace::focus event');
+ is($focus[0]->{current}->{name}, 'ws-right', 'focus event gave the right workspace');
+ is(@{$focus[0]->{current}->{nodes}}, $want, 'focus event gave the right number of windows on the workspace');
+}
+
+# move a window over to the right output
+subtest 'move right (1)', \&focus_subtest, 'move right', 1;
-$focus = AnyEvent->condvar;
-cmd 'move right';
-$event = $focus->recv;
-ok($event, 'moving from workspace with one window triggered focus ipc event');
-is($event->{current}->{name}, 'ws-right', 'focus event gave the right workspace');
-is(@{$event->{current}->{nodes}}, 2, 'focus event gave the right number of windows on the workspace');
+# move another window
+cmd 'workspace ws-left';
+subtest 'move right (2)', \&focus_subtest, 'move right', 2;
done_testing;
EOT
use i3test::XTEST;
-my ($cv, $timer);
-sub reset_test {
- $cv = AE::cv;
- $timer = AE::timer(1, 0, sub { $cv->send(0); });
-}
-
my $i3 = i3(get_socket_path());
$i3->connect()->recv;
my $ws = fresh_workspace;
-reset_test;
+my $cv = AnyEvent->condvar;
+my $timer = AnyEvent->timer(1, 0, sub { $cv->send(0) });
$i3->subscribe({
window => sub {
my ($event) = @_;
},
})->recv;
-my $con;
-
sub i3bar_present {
my ($nodes) = @_;
if (i3bar_present($i3->get_tree->recv->{nodes})) {
ok(1, 'i3bar present');
} else {
- $con = $cv->recv;
+ my $con = $cv->recv;
ok($con, 'i3bar appeared');
}
my $left = open_window;
my $right = open_window;
sync_with_i3;
-$con = $cv->recv;
+my $con = $cv->recv;
is($con->{window}, $right->{id}, 'focus is initially on the right container');
-reset_test;
-
-xtest_button_press(1, 3, 3);
-xtest_button_release(1, 3, 3);
-sync_with_i3;
-$con = $cv->recv;
-is($con->{window}, $left->{id}, 'button 1 moves focus left');
-reset_test;
-
-xtest_button_press(2, 3, 3);
-xtest_button_release(2, 3, 3);
-sync_with_i3;
-$con = $cv->recv;
-is($con->{window}, $right->{id}, 'button 2 moves focus right');
-reset_test;
-
-xtest_button_press(3, 3, 3);
-xtest_button_release(3, 3, 3);
-sync_with_i3;
-$con = $cv->recv;
-is($con->{window}, $left->{id}, 'button 3 moves focus left');
-reset_test;
-xtest_button_press(4, 3, 3);
-xtest_button_release(4, 3, 3);
-sync_with_i3;
-$con = $cv->recv;
-is($con->{window}, $right->{id}, 'button 4 moves focus right');
-reset_test;
+sub focus_subtest {
+ my ($subscribecb, $want, $msg) = @_;
+ my @events = events_for(
+ $subscribecb,
+ 'window');
+ my @focus = map { $_->{container}->{window} } grep { $_->{change} eq 'focus' } @events;
+ is_deeply(\@focus, $want, $msg);
+}
-xtest_button_press(5, 3, 3);
-xtest_button_release(5, 3, 3);
-sync_with_i3;
-$con = $cv->recv;
-is($con->{window}, $left->{id}, 'button 5 moves focus left');
-reset_test;
+subtest 'button 1 moves focus left', \&focus_subtest,
+ sub {
+ xtest_button_press(1, 3, 3);
+ xtest_button_release(1, 3, 3);
+ xtest_sync_with_i3;
+ },
+ [ $left->{id} ],
+ 'button 1 moves focus left';
+
+subtest 'button 2 moves focus right', \&focus_subtest,
+ sub {
+ xtest_button_press(2, 3, 3);
+ xtest_button_release(2, 3, 3);
+ xtest_sync_with_i3;
+ },
+ [ $right->{id} ],
+ 'button 2 moves focus right';
+
+subtest 'button 3 moves focus left', \&focus_subtest,
+ sub {
+ xtest_button_press(3, 3, 3);
+ xtest_button_release(3, 3, 3);
+ xtest_sync_with_i3;
+ },
+ [ $left->{id} ],
+ 'button 3 moves focus left';
+
+subtest 'button 4 moves focus right', \&focus_subtest,
+ sub {
+ xtest_button_press(4, 3, 3);
+ xtest_button_release(4, 3, 3);
+ xtest_sync_with_i3;
+ },
+ [ $right->{id} ],
+ 'button 4 moves focus right';
+
+subtest 'button 5 moves focus left', \&focus_subtest,
+ sub {
+ xtest_button_press(5, 3, 3);
+ xtest_button_release(5, 3, 3);
+ xtest_sync_with_i3;
+ },
+ [ $left->{id} ],
+ 'button 5 moves focus left';
done_testing;