Forráskód Böngészése

Add convenience wait function: `wait_until`

It accepts a BLOCK that can be executed repeatedly; if the BLOCK ever
evals to true, you get the result back, otherwise you get '' back after
thirty seconds. It unwisely suppresses all attempts to die or croak, so
please check the return value before proceeding!
Daniel Gempesaw 10 éve
szülő
commit
0c3f19b220
3 módosított fájl, 164 hozzáadás és 0 törlés
  1. 1 0
      cpanfile
  2. 111 0
      lib/Selenium/Waiter.pm
  3. 52 0
      t/Waiter.t

+ 1 - 0
cpanfile

@@ -41,6 +41,7 @@ on 'test' => sub {
   requires "Test::Fatal" => "0";
   requires "Test::LWP::UserAgent" => "0";
   requires "Test::More" => "0";
+  requires "Test::Warn" => "0";
   requires "lib" => "0";
 };
 

+ 111 - 0
lib/Selenium/Waiter.pm

@@ -0,0 +1,111 @@
+package Selenium::Waiter;
+
+# ABSTRACT: Provides a utility wait_until function
+use Try::Tiny;
+require Exporter;
+our @ISA = qw/Exporter/;
+our @EXPORT = qw/wait_until/;
+
+=head1 SYNOPSIS
+
+    use Selenium::Waiter qw/wait_until/;
+    my $d = Selenium::Remote::Driver->new;
+
+    my $div = wait_until { $d->find_element('div', css') };
+
+=func wait_until
+
+Exported by default, it takes a BLOCK (required) and optionally a
+hash of configuration params. It uses a prototype to take its
+arguments, so usage looks look like:
+
+    use Selenium::Waiter;
+    my $div = wait_until { $driver->find_element('div', css') };
+
+The above snippet will search for C<css=div> for thirty seconds; if it
+ever finds the element, it will immediately return. More generally,
+Once the BLOCK returns anything truthy, the C<wait_until> will stop
+evaluating and the return of the BLOCK will be returned to you. If the
+BLOCK never returns a truthy value, we'll wait until the elapsed time
+has increased past the timeout and then return an empty string C<''>.
+
+B<Achtung!> Please make sure that the BLOCK you pass in can be
+executed in a timely fashion. For Webdriver, that means that you
+should set the appropriate C<implicit_wait> timeout low (a second or
+less!)  so that we can rerun the assert sub repeatedly. We don't do
+anything fancy behind the scenes: we just execute the BLOCK you pass
+in and sleep between iterations. If your BLOCK actively blocks for
+thirty seconds, like a C<find_element> would do with an
+C<implicit_wait> of 30 seconds, we won't be able to help you at all -
+that blocking behavior is on the webdriver server side, and is out of
+our control. We'd run one iteration, get blocked for thirty seconds,
+and return control to you at that point.
+
+=head4 Dying
+
+PLEASE check the return value before proceeding, as we unwisely
+suppress any attempts your BLOCK may make to die or croak. The BLOCK
+you pass is called in a L<try/Try::Tiny>, and if any of the
+invocations of your function throw and the BLOCK never becomes true,
+we'll carp exactly once at the end immediately before returning
+false. We overwrite the death message from each iteration, so at the
+end, you'll only see the most recent death message.
+
+    # warns once after thirty seconds: "kept from dying";
+    wait_until { die 'kept from dying' };
+
+The output of C<die>s from each iteration can be exposed if you wish
+to see the massacre:
+
+    # carps: "kept from dying" once a second for thirty seconds
+    wait_until { die 'kept from dying' } debug => 1;
+
+=head4 Timeouts and Intervals
+
+You can also customize the timeout, and/or the retry interval between
+iterations.
+
+    # prints hi three four times at 0, 3, 6, and 9 seconds
+    wait_until { print 'hi'; '' } timeout => 10, interval => 3;
+
+=cut
+
+sub wait_until (&%) {
+    my $assert = shift;
+    my $args = {
+        timeout => 30,
+        interval => 1,
+        debug => 0,
+        @_
+    };
+
+    my $start = time;
+    my $timeout_not_elapsed = sub {
+        my $elapsed = time - $start;
+        return $elapsed < $args->{timeout};
+    };
+
+    my $exception = '';
+    while ($timeout_not_elapsed->()) {
+        my $try_ret = try {
+            my $assert_ret = $assert->();
+            return $assert_ret if $assert_ret;
+        }
+        catch {
+            $exception = $_;
+            warn $_ if $args->{debug};
+            return '';
+        }
+        finally {
+            sleep($args->{interval});
+        };
+
+        return $try_ret if $try_ret;
+    }
+
+    # No need to repeat ourselves if we're already debugging.
+    warn $exception if $exception && ! $args->{debug};
+    return '';
+}
+
+1;

+ 52 - 0
t/Waiter.t

@@ -0,0 +1,52 @@
+#! /usr/bin/perl
+
+use strict;
+use warnings;
+use Test::More;
+use Test::Warn;
+use Test::Fatal;
+use Selenium::Waiter;
+
+
+SIMPLE_WAIT: {
+    my $ret;
+    waits_ok( sub { $ret = wait_until { 1 } }, '<', 2, 'immediately true returns quickly' );
+    ok($ret == 1, 'return value for a true wait_until is passed up');
+    waits_ok( sub { $ret = wait_until { 0 } timeout => 3 }, '>', 2, 'never true expires the timeout' );
+    ok($ret eq '', 'return value for a false wait is an empty string');
+}
+
+EVENTUALLY: {
+    my $ret = 0;
+    waits_ok( sub { wait_until { $ret++ > 2 } }, '>', 2, 'eventually true takes time');
+
+    $ret = 0;
+    my %opts = ( interval => 2, timeout => 5 );
+    waits_ok(
+        sub { wait_until { $ret++; 0 } %opts }, '>', 5,
+        'timeout is respected'
+    );
+    ok($ret == 3, 'interval option changes iteration speed');
+}
+
+EXCEPTIONS: {
+    my %opts = ( timeout => 2 );
+    warning_is { wait_until { die 'caught!' } %opts } 'caught!',
+      'exceptions usually only warn once';
+
+    my %debug = ( debug => 1, %opts );
+    warnings_are { wait_until { die 'caught!' } %debug } ['caught!', 'caught!'],
+      'exceptions warn repreatedly when in debug mode';
+}
+
+sub waits_ok  {
+    my ($sub, $cmp, $expected_duration, $test_desc) = @_;
+
+    my $start = time;
+    $sub->();
+    my $elapsed = time - $start;
+
+    cmp_ok($elapsed, $cmp, $expected_duration, $test_desc);
+}
+
+done_testing;