Implement the tick event

This makes our tests less flaky, shorter, and more readable.

fixes #2988
This commit is contained in:
Michael Stapelberg 2017-09-24 15:40:30 +02:00
parent 14c8cf8622
commit ce21de8dde
29 changed files with 457 additions and 734 deletions

View File

@ -99,11 +99,12 @@ use constant TYPE_GET_BAR_CONFIG => 6;
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} } );
@ -120,6 +121,7 @@ my %events = (
barconfig_update => ($event_mask | 4),
binding => ($event_mask | 5),
shutdown => ($event_mask | 6),
tick => ($event_mask | 7),
_error => 0xFFFFFFFF,
);
@ -519,6 +521,18 @@ sub get_config {
$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)

View File

@ -64,6 +64,7 @@ to do that).
| 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:
@ -126,6 +127,8 @@ BINDING_MODES (8)::
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
@ -637,6 +640,19 @@ which is a string containing the config file as loaded by i3 most recently.
{ "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
@ -694,6 +710,10 @@ binding (5)::
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:*
--------------------------------------------------------------------
@ -866,6 +886,27 @@ because of a user action such as a +restart+ or +exit+ command. The +change
}
---------------------------
=== 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]]

View File

@ -207,9 +207,11 @@ int main(int argc, char *argv[]) {
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') {

View File

@ -60,6 +60,9 @@ typedef struct i3_ipc_header {
/** 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
*
@ -74,6 +77,7 @@ typedef struct i3_ipc_header {
#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.
@ -101,3 +105,6 @@ typedef struct i3_ipc_header {
/** 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)

View File

@ -31,6 +31,10 @@ typedef struct ipc_client {
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;

View File

@ -1046,8 +1046,9 @@ static int add_subscription(void *extra, const unsigned char *s,
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;
@ -1099,6 +1100,25 @@ IPC_HANDLER(subscribe) {
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);
}
/*
@ -1122,9 +1142,35 @@ IPC_HANDLER(get_config) {
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,
@ -1135,6 +1181,7 @@ handler_t handlers[10] = {
handle_get_version,
handle_get_binding_modes,
handle_get_config,
handle_send_tick,
};
/*

View File

@ -47,6 +47,8 @@ our @EXPORT = qw(
wait_for_unmap
$x
kill_all_windows
events_for
listen_for_binding
);
=head1 NAME
@ -900,6 +902,86 @@ sub kill_all_windows {
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 well 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>

View File

@ -20,8 +20,6 @@ our @EXPORT = qw(
xtest_key_release
xtest_button_press
xtest_button_release
listen_for_binding
start_binding_capture
binding_events
);
@ -256,86 +254,6 @@ sub import {
=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

View File

@ -16,61 +16,25 @@
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;

View File

@ -28,24 +28,11 @@ mode "with spaces" {
}
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;

View File

@ -16,46 +16,15 @@
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 @events = events_for(
sub { open_window },
'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');
}
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;

View File

@ -16,13 +16,6 @@
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
################################
@ -33,62 +26,29 @@ my $win0 = open_window;
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');
sub focus_subtest {
my ($cmd, $name) = @_;
$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');
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 1', 'Window 1 focused');
$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');
$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;

View File

@ -16,42 +16,17 @@
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;
my @events = events_for(
sub {
$window->name('New Window Title');
sync_with_i3;
},
'window');
$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');
}
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;

View File

@ -19,41 +19,19 @@
# 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;

View File

@ -18,12 +18,8 @@
#
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 {
@ -35,26 +31,17 @@ 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;
my @events = events_for(
sub { cmd "workspace $ws2" },
'workspace');
cmd "workspace $ws2";
sync_with_i3;
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 {
@ -63,36 +50,16 @@ 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;
my @events = events_for(
sub { cmd "workspace $ws2" },
'workspace');
# 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";
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 {
@ -101,25 +68,16 @@ 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;
my @events = events_for(
sub {
$w1->unmap;
sync_with_i3;
},
'workspace');
cmd '[id="' . $w1->id . '"] kill';
sync_with_i3;
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;

View File

@ -19,41 +19,22 @@
# 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;

View File

@ -35,51 +35,38 @@ SKIP: {
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 = AnyEvent->condvar;
my $cv = AE::cv;
my $timer = AE::timer 0.5, 0, sub { $cv->send(0); };
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');
$i3->subscribe({
binding => sub {
$cv->send(shift);
}
})->recv;
is(scalar @events, 1, 'Received 1 event');
qx(xdotool key $binding_symbol);
my $e = $cv->recv;
does_i3_live;
diag "Event:\n", Dumper($e);
ok($e,
'the binding event should emit when user input triggers an i3 binding 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);

View File

@ -36,8 +36,6 @@ SKIP: {
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(
@ -87,9 +85,6 @@ 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|);

View File

@ -37,8 +37,6 @@ SKIP: {
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
@ -87,9 +85,6 @@ is(listen_for_binding(
'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;

View File

@ -28,23 +28,11 @@ EOT
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;

View File

@ -18,27 +18,16 @@
# 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.
@ -46,11 +35,7 @@ $timer = AnyEvent->timer(
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.
@ -59,11 +44,7 @@ fresh_workspace;
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';
###############################################################################

View File

@ -19,34 +19,17 @@
# 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;

View File

@ -19,43 +19,22 @@
# 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;
sub move_subtest {
my ($cmd) = @_;
my $cv = AnyEvent->condvar;
my @events = events_for(
sub { cmd $cmd },
'window');
ok($con, 'moving a window should emit the window::move event');
is($con->{window}, $window->{id}, 'the event should contain info about the window');
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');
}
reset_test;
cmd 'move to workspace ws_new';
$con = $cv->recv;
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;

View File

@ -19,50 +19,37 @@
#
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;
sub urgency_subtest {
my ($subscribecb, $win, $want) = @_;
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');
my @events = events_for(
$subscribecb,
'window');
$cv = AnyEvent->condvar;
$win->delete_hint('urgency');
$event = $cv->recv;
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");
}
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');
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;

View File

@ -23,13 +23,13 @@
# 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({
@ -50,7 +50,7 @@ is($e->{change}, 'restart', 'the `change` field should tell the reason for the s
$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({
@ -66,6 +66,5 @@ $e = $cv->recv;
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;

View File

@ -51,8 +51,6 @@ EOT
my $pid = launch_with_config($config);
start_binding_capture;
is(listen_for_binding(
sub {
xtest_key_press(87); # KP_End
@ -213,9 +211,6 @@ is(listen_for_binding(
'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);
################################################################################
@ -234,8 +229,6 @@ EOT
$pid = launch_with_config($config);
start_binding_capture;
is(listen_for_binding(
sub {
xtest_key_press(133); # Super_L
@ -288,9 +281,6 @@ is(listen_for_binding(
'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);
################################################################################
@ -307,8 +297,6 @@ EOT
$pid = launch_with_config($config);
start_binding_capture;
is(listen_for_binding(
sub {
xtest_key_press(87); # KP_End
@ -329,7 +317,7 @@ is(listen_for_binding(
'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
@ -339,11 +327,10 @@ is(listen_for_binding(
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
@ -353,15 +340,12 @@ is(listen_for_binding(
xtest_key_release(77); # disable Num_Lock
xtest_sync_with_i3;
},
),
'timeout',
'Did not trigger the KP_Down keybinding with KP_2');
'binding');
is(scalar @unexpected2, 0, 'Did not trigger the KP_Down keybinding with KP_2');
# TODO: This test does not verify that i3 does _NOT_ grab keycode 87 with Mod2.
sync_with_i3;
is(scalar @i3test::XTEST::binding_events, 18, 'Received exactly 18 binding events');
exit_gracefully($pid);
################################################################################
@ -379,8 +363,6 @@ $pid = launch_with_config($config);
my $win = open_window;
start_binding_capture;
is(listen_for_binding(
sub {
xtest_key_press(77); # enable Num_Lock

View File

@ -13,7 +13,7 @@
#
# • 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
@ -23,46 +23,17 @@ font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
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;

View File

@ -27,51 +27,29 @@ workspace ws-left output fake-0
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;
sub focus_subtest {
my ($cmd, $want) = @_;
my @events = events_for(
sub { cmd $cmd },
'workspace');
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
cmd 'move right';
my $event = $focus->recv;
subtest 'move right (1)', \&focus_subtest, 'move right', 1;
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');
# reset and try again
$focus = AnyEvent->condvar;
# move another window
cmd 'workspace ws-left';
$focus->recv;
$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');
subtest 'move right (2)', \&focus_subtest, 'move right', 2;
done_testing;

View File

@ -34,17 +34,12 @@ bar {
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) = @_;
@ -60,8 +55,6 @@ $i3->subscribe({
},
})->recv;
my $con;
sub i3bar_present {
my ($nodes) = @_;
@ -83,50 +76,68 @@ sub i3bar_present {
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;
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(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;
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';
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;
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';
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;
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';
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 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;