Group
Extension

Test-Mojo-Role-Selenium/lib/Test/Mojo/Role/Selenium.pm

package Test::Mojo::Role::Selenium;
use Mojo::Base -base;
use Role::Tiny;

use Carp 'croak';
use File::Basename ();
use File::Spec;
use Mojo::Parameters;
use Mojo::Util 'encode';
use Selenium::Remote::WDKeys ();

use constant DEBUG         => $ENV{MOJO_SELENIUM_DEBUG}         || 0;
use constant WAIT_INTERVAL => $ENV{MOJO_SELENIUM_WAIT_INTERVAL} || 0.5;
use constant WAIT_TIMEOUT  => $ENV{MOJO_SELENIUM_WAIT_TIMEOUT}  || 60;

$ENV{TEST_SELENIUM} //= '0';
$ENV{MOJO_SELENIUM_BASE_URL} ||= $ENV{TEST_SELENIUM} =~ /^http/ ? $ENV{TEST_SELENIUM} : '';

sub S { Mojo::JSON::encode_json($_[0]) }

our $VERSION = '0.17';

my $SCRIPT_NAME = File::Basename::basename($0);
my $SCREENSHOT  = 1;

has driver => sub {
  my $self = shift;
  my $args = $self->driver_args;
  my ($driver, $env) = split /\&/, +($ENV{MOJO_SELENIUM_DRIVER} || ''), 2;
  $env = Mojo::Parameters->new($env || '')->to_hash;
  $driver ||= $args->{driver_class} || 'Selenium::Chrome';
  eval "require $driver;1" or croak "require $driver: $@";
  warn "[Selenium] Using $driver\n" if DEBUG;
  $driver = $driver->new(%$args, %$env, ua => $self->ua);
  $driver->debug_on if DEBUG > 1;
  $driver->default_finder('css');
  $driver;
};

has driver_args          => sub { +{} };
has screenshot_directory => sub { File::Spec->tmpdir };
has screenshots          => sub { +[] };

has _live_base => sub {
  my $self = shift;
  return Mojo::URL->new($ENV{MOJO_SELENIUM_BASE_URL}) if $ENV{MOJO_SELENIUM_BASE_URL};
  $self->{live_port} = Mojo::IOLoop::Server->generate_port;
  my $test_hostname = $ENV{MOJO_SELENIUM_TEST_HOST} || '127.0.0.1';
  return Mojo::URL->new("http://${test_hostname}:$self->{live_port}");
};

has _live_server => sub {
  my $self   = shift;
  my $app    = $self->app or croak 'Cannot start server without $t->app(...) set';
  my $server = Mojo::Server::Daemon->new(silent => DEBUG ? 0 : 1);

  Scalar::Util::weaken($self);
  $server->on(
    request => sub {
      my ($server, $tx) = @_;
      $self->tx($tx) if $tx->req->url->to_abs eq $self->_live_url;
    }
  );

  $server->app($app)->listen([$self->_live_base->to_string])
    ->start->ioloop->acceptor($server->acceptors->[0]);

  return $server;
};

has _live_url => sub { Mojo::URL->new };

sub active_element_is {
  my ($self, $selector, $desc) = @_;
  my $driver = $self->driver;
  my $active = $driver->get_active_element;
  my $el     = $self->_find_element($selector);
  my $same   = $active && $el ? $driver->compare_elements($active, $el) : 0;

  return $self->test('ok', $same, _desc($desc, "active element is @{[S $selector]}"));
}

sub capture_screenshot {
  my ($self, $path) = @_;
  $path = _screenshot_name($path ? "$path.png" : "%0-%t-%n.png");
  $path = File::Spec->catfile($self->screenshot_directory, $path);
  Test::More::diag("Saving screenshot to $path");
  $self->driver->capture_screenshot($path);
  push @{$self->screenshots}, $path;
  return $self;
}

sub click_ok {
  my ($self, $selector) = @_;
  my $el  = $selector ? $self->_find_element($selector) : $self->driver->get_active_element;
  my $err = 'no such element';

  if ($el) {
    eval { $self->driver->mouse_move_to_location(element => $el) } unless $el->is_displayed;
    $err = $@ || 'unable to click';
    $err = '' if $el->click;
  }

  return $self->test('ok', !$err, _desc("click on @{[S $selector]} $err"));
}

sub current_url_is {
  my $self = shift;
  my $url  = $self->_live_abs_url(shift);

  return $self->test('is', $self->driver->get_current_url,
    $url->to_string, _desc('exact match for current url'));
}

sub current_url_like {
  my ($self, $match, $desc) = @_;
  return $self->test('like', $self->driver->get_current_url,
    $match, _desc($desc, 'current url is similar'));
}

sub element_is_displayed {
  my ($self, $selector, $desc) = @_;
  my $el = $self->_find_element($selector);
  return $self->test(
    'ok',
    ($el && $el->is_displayed),
    _desc($desc, "element @{[S $selector]} is displayed")
  );
}

sub element_is_hidden {
  my ($self, $selector, $desc) = @_;
  my $el = $self->_find_element($selector);
  return $self->test('ok', ($el && $el->is_hidden),
    _desc($desc, "element @{[S $selector]} is hidden"));
}

sub go_back    { $_[0]->_proxy('go_back');    $_[0] }
sub go_forward { $_[0]->_proxy('go_forward'); $_[0] }

sub if_tx {
  my ($self, $method) = (shift, shift);

SKIP: {
    my $desc = ref $method ? '__SUB__' : $method;
    Test::More::skip("\$t->tx() is not defined ($desc)", 1) unless $self->tx;
    $self->$method(@_);
  }

  return $self;
}

sub live_element_count_is {
  my ($self, $selector, $count, $desc) = @_;
  my $els = $self->_proxy(find_elements => $selector);
  return $self->test('is', int(@$els), $count,
    _desc($desc, "element count for selector @{[S $selector]}"));
}

sub live_element_exists {
  my ($self, $selector, $desc) = @_;
  $desc = _desc($desc, "element for selector @{[S $selector]} exists");
  return $self->test('ok', $self->_find_element($selector), $desc);
}

sub live_element_exists_not {
  my ($self, $selector, $desc) = @_;
  $desc = _desc($desc, "no element for selector @{[S $selector]}");
  return $self->test('ok', !$self->_find_element($selector), $desc);
}

sub live_text_is {
  my ($self, $selector, $value, $desc) = @_;
  return $self->test(
    'is',   $self->_element_data(get_text => $selector),
    $value, _desc($desc, "exact text for selector @{[S $selector]}")
  );
}

sub live_text_like {
  my ($self, $selector, $regex, $desc) = @_;
  return $self->test(
    'like', $self->_element_data(get_text => $selector),
    $regex, _desc($desc, "similar text for selector @{[S $selector]}")
  );
}

sub live_value_is {
  my ($self, $selector, $value, $desc) = @_;
  return $self->test(
    'is',   $self->_element_data(get_value => $selector),
    $value, _desc($desc, "exact value for selector @{[S $selector]}")
  );
}

sub live_value_like {
  my ($self, $selector, $regex, $desc) = @_;
  return $self->test(
    'like', $self->_element_data(get_value => $selector),
    $regex, _desc($desc, "similar value for selector @{[S $selector]}")
  );
}

sub navigate_ok {
  my $self = shift;
  my $url  = $self->_live_abs_url(shift);
  my ($desc, $err);

  $self->tx(undef)->_live_url($url);
  $self->_live_server if $self->{live_port};    # Make sure server is running
  $self->driver->get($url->to_string);

  if ($self->tx) {
    $desc = "navigate to $url";
    $err  = $self->tx->error;
    Test::More::diag($err->{message}) if $err and $err->{message};
  }
  else {
    $desc = "navigate to $url (\$t->tx() is not set)";
  }

  return $self->test('ok', !$err, _desc($desc));
}

sub new {
  my $self = shift->SUPER::new;
  $self->ua(Test::Mojo::Role::Selenium::UserAgent->new->ioloop(Mojo::IOLoop->singleton));
  return $self if $ENV{MOJO_SELENIUM_BASE_URL};
  return $self unless my $app = shift;
  my @args = @_ ? {config => {config_override => 1, %{shift()}}} : ();
  return $self->app(ref $app ? $app : Mojo::Server->new->build_app($app, @args));
}

sub refresh { $_[0]->_proxy('refresh'); $_[0] }

sub send_keys_ok {
  my ($self, $selector, $keys, $desc) = @_;
  my $el = $selector ? $self->_find_element($selector) : $self->driver->get_active_element;

  $selector ||= 'active element';
  $keys = [ref $keys ? $keys : split //, $keys] unless ref $keys eq 'ARRAY';

  for (@$keys) {
    my $key = ref $_ ? Selenium::Remote::WDKeys::KEYS()->{$$_} : $_;
    croak "Invalid key '@{[ref $_ ? $$_ : $_]}'" unless defined $key;
    $_ = $key;
  }

  if ($el) {
    eval {
      for my $key (@$keys) {
        warn "[Selenium] send_keys $selector <- @{[Mojo::Util::url_escape($key)]}\n" if DEBUG;
        $el->send_keys($key);
      }
      1;
    } or do {
      Test::More::diag($@);
      $el = undef;
    };
  }

  $keys = Mojo::Util::url_escape(join '', @$keys);
  return $self->test('ok', $el, _desc($desc, "keys ($keys) sent to @{[S $selector]}"));
}

sub set_window_size {
  my ($self, $size, $desc) = @_;
  $self->driver->set_window_size(reverse @$size);
  return $self;
}

sub setup_or_skip_all {
  my $self = shift;

  local $@;
  Test::More::plan(skip_all => $@ || 'TEST_SELENIUM=1 or TEST_SELENIUM=http://...')
    unless $ENV{TEST_SELENIUM} and eval { $self->driver };

  $ENV{MOJO_SELENIUM_BASE_URL} ||= $ENV{TEST_SELENIUM} if $ENV{TEST_SELENIUM} =~ /^http/;

  return $self;
}

sub submit_ok {
  my ($self, $selector, $desc) = @_;
  my $el = $self->_find_element($selector);
  $el->submit if $el;
  return $self->test('ok', $el, _desc($desc, "click on @{[S $selector]}"));
}

sub toggle_checked_ok {
  my ($self, $selector) = @_;
  my $el = $self->_find_element($selector);

  if ($el) {
    if ($el->is_displayed) {
      $el->click;
    }
    else {
      my $sel = $selector;
      $sel =~ s!"!\\"!g;
      $self->driver->execute_script(
        qq[var el=document.querySelector("$sel");el.setAttribute("checked", !el.getAttribute("checked"))]
      );
    }
  }

  return $self->test('ok', $el, _desc("click on @{[S $selector]}"));
}

sub wait_for {
  my $self = shift;
  my $sel  = shift;
  my $args = ref $_[0] eq 'HASH' ? shift : {};
  my $desc = shift || "waited for element $sel";
  my @checks;

  if (Scalar::Util::looks_like_number($sel)) {
    $self->ua->ioloop->timer($sel => sub { shift->stop });
    $self->ua->ioloop->start;
    return $self;
  }

  push @checks, 'is_displayed' if $sel =~ s!:visible\b!!;
  push @checks, 'is_enabled'   if $sel =~ s!:enabled\b!!;
  push @checks, 'is_hidden'    if $sel =~ s!:hidden\b!!;
  push @checks, 'is_selected'  if $sel =~ s!:selected\b!!;

  my $driver                = $self->driver;
  my $prev_implicit_timeout = $driver->get_timeouts->{implicit};
  $driver->set_timeout(implicit => $args->{timeout} || WAIT_TIMEOUT);

  my $ok;
  $self->wait_until(
    sub {
      my $e = $_->find_element($sel);
      return $ok = $e && @checks == grep { $e->$_ } @checks;
    },
    {%$args, skip => 1},
  );

  $driver->set_timeout(implicit => $prev_implicit_timeout);

  return $self->test('ok', $ok, _desc($desc));
}

sub wait_until {
  my ($self, $cb, $args) = @_;
  my $ioloop = $self->ua->ioloop;

  my $err;
  my @tid = (
    $ioloop->timer($args->{timeout} || WAIT_TIMEOUT, sub { $err = 'Timeout' }),
    $ioloop->recurring(
      $args->{interval} || WAIT_INTERVAL,
      sub {
        return shift->stop if $err || eval { local $_ = $self->driver; $self->$cb($args) };
        Test::More::diag("[Selenium] wait_until: $@") if $@ and ($args->{debug} or DEBUG);
      }
    ),
  );

  my $t0 = time;
  $ioloop->start;
  $ioloop->remove($_) for @tid;

  return $self if $args->{skip};
  return $self->test('ok', !$err, _desc($args->{desc} || "waited for @{[time - $t0]}s"));
}

sub window_size_is {
  my ($self, $exp, $desc) = @_;
  my $size = $self->driver->get_window_size;

  return $self->test('is_deeply', [@$size{qw(width height)}],
    $exp, _desc($desc, "window size is $exp->[0]x$exp->[1]"));
}

sub _desc { encode 'UTF-8', shift || shift }

sub _find_element {
  my ($self, $selector) = @_;
  return $self->_proxy(find_element => $selector) unless ref $selector;

  my ($by) = keys %$selector;
  return $self->_proxy("find_element_by_$by" => $selector->{$by}) unless ref $selector;
}

sub _live_abs_url {
  my $self = shift;
  my $url  = Mojo::URL->new(shift);

  unless ($url->is_abs) {
    my $base = $self->_live_base;
    $url->scheme($base->scheme)->host($base->host)->port($base->port);
  }

  return $url;
}

sub _proxy {
  my ($self, $method) = (shift, shift);
  my $res = eval { $self->driver->$method(@_) };
  warn $@ if DEBUG and $@;
  return $res;
}

sub _element_data {
  my ($self, $method) = (shift, shift);
  my $el = $self->_find_element(shift);
  return $el ? $el->$method : '';
}

sub _screenshot_name {
  local $_ = shift;
  s!\%0\b!{$SCRIPT_NAME}!ge;
  s!\%n\b!{sprintf '%04s', $SCREENSHOT++}!ge;
  s!\%t\b!{$^T}!ge;
  return $_;
}

package    # hide from pause
  Test::Mojo::Role::Selenium::UserAgent;
use Mojo::Base 'Mojo::UserAgent';

use constant DEBUG => $ENV{MOJO_SELENIUM_DEBUG} || 0;

sub request {
  my ($ua, $req) = @_;
  my $method = uc($req->method || 'get');
  my $tx = $ua->build_tx($method, $req->uri->as_string, {$req->headers->flatten}, $req->content);
  my $done;

  warn "[Selenium] $method @{[$req->uri->as_string]}\n" if DEBUG;

  # This is super ugly and need to be implemented differently,
  # but I'm not sure how to implement wait_until() without this
  # one_tick() hack.
  if ($ua->ioloop->is_running) {
    $ua->start($tx, sub { $done = 1 });
    $ua->ioloop->reactor->one_tick until $done;
  }
  else {
    $ua->start($tx);
  }

  return HTTP::Response->parse($tx->res->to_string);
}

# Should not say "... during global destruction."
# sub DESTROY { warn 'no circular refs?' }

1;

=encoding utf8

=head1 NAME

Test::Mojo::Role::Selenium - Test::Mojo in a real browser

=head1 SYNOPSIS

=head2 External app

  use Mojo::Base -strict;
  use Test::More;

  $ENV{MOJO_SELENIUM_BASE_URL} ||= 'http://mojolicious.org';
  $ENV{MOJO_SELENIUM_DRIVER}   ||= 'Selenium::Chrome';

  my $t = Test::Mojo->with_roles("+Selenium")->new->setup_or_skip_all;

  $t->navigate_ok('/perldoc')
    ->live_text_is('a[href="#GUIDES"]' => 'GUIDES');

  $t->driver->execute_script(qq[document.querySelector("form").removeAttribute("target")]);
  $t->element_is_displayed("input[name=q]")
    ->send_keys_ok("input[name=q]", ["render", \"return"]);

  $t->wait_until(sub { $_->get_current_url =~ qr{q=render} })
    ->live_value_is("input[name=search]", "render");

  done_testing;

=head2 Internal app

  use Mojo::Base -strict;
  use Test::More;

  my $t = Test::Mojo->with_roles("+Selenium")->new("MyApp")->setup_or_skip_all;

  # All the standard Test::Mojo methods are available
  ok $t->isa("Test::Mojo");
  ok $t->does("Test::Mojo::Role::Selenium");

  $t->navigate_ok("/")
    ->status_is(200)
    ->header_is("Server" => "Mojolicious (Perl)")
    ->text_is("div#message" => "Hello!")
    ->live_text_is("div#message" => "Hello!")
    ->live_element_exists("nav")
    ->element_is_displayed("nav")
    ->active_element_is("input[name=q]")
    ->send_keys_ok("input[name=q]", "Mojo")
    ->capture_screenshot;

  $t->submit_ok("form")
    ->status_is(200)
    ->current_url_like(qr{q=Mojo})
    ->live_element_exists("input[name=q][value=Mojo]");

  $t->click_ok("nav a.logo")->status_is(200);

  done_testing;

=head1 DESCRIPTION

L<Test::Mojo::Role::Selenium> is a role that extends L<Test::Mojo> with
additional methods which checks behaviour in a browser. All the heavy lifting
is done by L<Selenium::Remote::Driver>.

Some of the L<Selenium::Remote::Driver> methods are available directly in this
role, while the rest are available through the object held by the L</driver>
attribute. Please create an issue if you think more tests or methods should be
provided directly by L<Test::Mojo::Role::Selenium>.

=head1 OPTIONAL DEPENDENCIES

L<Selenium::Remote::Driver> require some external dependencies to work. Here
are a quick intro to install some of the dependencies to make this module work.

=over 2

=item * L<Selenium::Chrome>

  # macOS
  $ brew install chromedriver

  # Ubuntu
  $ sudo apt-get install chromium-chromedriver

  # Run tests
  $ MOJO_SELENIUM_DRIVER=Selenium::Chrome prove -l

=back

=head1 CAVEAT

L<Test::Mojo/tx> is only populated, if the request went through an L</Internal app>.
This means that methods such as L<Test::Mojo/header_is> will not work or
probably fail completely when testing an L</External app>.

=head1 ENVIRONMENT VARIABLES

=head2 MOJO_SELENIUM_BASE_URL

Setting this variable will make this test send the requests to a remote server,
instead of starting a local server. Note that this will disable L<Test::Mojo>
methods such as L</status_is>, since L<Test::Mojo/tx> will not be set. See
also L</CAVEAT>.

This variable will get the value of L</TEST_SELENIUM> if it looks like a URL.

=head2 MOJO_SELENIUM_TEST_HOST

In some cases you may want to override the host of your test server, when
running Selenium on a separate server or in a pod-style networking environment
this still retains the automatically generated port. This will not disable the
L<Test::Mojo> methods.

=head2 MOJO_SELENIUM_DRIVER

This variable can be set to a classname, such as L<Selenium::Chrome> which will
force the selenium driver. It can also be used to pass on arguments to the
driver's constructor. Example:

  MOJO_SELENIUM_DRIVER='Selenium::Remote::Driver&browser_name=firefox&port=4444'

The arguments will be read using L<Mojo::Parameters/parse>, which means they
follow standard URL format rules.

=head2 TEST_SELENIUM

This variable must be set to a true value for L</setup_or_skip_all> to not skip
this test. Will also set L</MOJO_SELENIUM_BASE_URL> if it looks like an URL.

=head1 ATTRIBUTES

=head2 driver

  $driver = $self->driver;

An instance of L<Selenium::Remote::Driver>.

=head2 driver_args

  $hash = $self->driver_args;
  $self = $self->driver_args({driver_class => "Selenium::Chrome"});

Used to set args passed on to the L</driver> on construction time. In addition,
a special key "driver_class" can be set to use another driver class, than the
default.

Note that the environment variavble C<MOJO_SELENIUM_DRIVER> can also be used to
override the driver class.

=head2 screenshot_directory

  $path = $self->screenshot_directory;
  $self = $self->screenshot_directory(File::Spec->tmpdir);

Where screenshots are saved.

=head2 screenshots

  $array = $self->screenshots;

Holds an array ref with paths to all the screenshots taken with
L</capture_screenshot>.

=head1 METHODS

=head2 active_element_is

  $self = $self->active_element_is("input[name=username]");

Checks that the current active element on the page match the selector.

=head2 capture_screenshot

  $self = $self->capture_screenshot;
  $self = $self->capture_screenshot("%t-page-x");
  $self = $self->capture_screenshot("%0-%t-%n"); # default

Capture screenshot to L</screenshot_directory> with filename specified by the
input format. The format supports these special strings:

  Format | Description
  -------|----------------------
  %t     | Start time for script
  %0     | Name of script
  %n     | Auto increment

=head2 click_ok

  $self = $self->click_ok("a");
  $self = $self->click_ok;

Click on an element matching the selector or click on the currently active
element.

=head2 current_url_is

  $self = $self->current_url_is("http://mojolicious.org/");
  $self = $self->current_url_is("/whatever");

Test the current browser URL against an absolute URL. A relative URL will be
converted to an absolute URL, using L</MOJO_SELENIUM_BASE_URL>.

=head2 current_url_like

  $self = $self->current_url_like(qr{/whatever});

Test the current browser URL against a regex.

=head2 element_is_displayed

  $self = $self->element_is_displayed("nav");

Test if an element is displayed on the web page.

See L<Selenium::Remote::WebElement/is_displayed>.

=head2 element_is_hidden

  $self = $self->element_is_hidden("nav");

Test if an element is hidden on the web page.

See L<Selenium::Remote::WebElement/is_hidden>.

=head2 go_back

  $self = $self->go_back;

Equivalent to hitting the back button on the browser.

See L<Selenium::Remote::Driver/go_back>.

=head2 go_forward

  $self = $self->go_forward;

Equivalent to hitting the forward button on the browser.

See L<Selenium::Remote::Driver/go_forward>.

=head2 if_tx

  $self = $self->if_tx(sub { ... }, @args);
  $self = $self->if_tx($method, @args);

Call either a code ref or a method on C<$self> if L<Test::Mojo/tx> is defined.
C<tx()> is undefined if L</navigate_ok> is called on an external resource.

Examples:

  $self->if_tx(status_is => 200);

=head2 live_element_count_is

  $self = $self->live_element_count_is("a", 12);

Checks that the selector finds the correct number of elements in the browser.

See L<Test::Mojo/element_count_is>.

=head2 live_element_exists

  $self = $self->live_element_exists("div.content");

Checks that the selector finds an element in the browser.

See L<Test::Mojo/element_exists>.

=head2 live_element_exists_not

  $self = $self->live_element_exists_not("div.content");

Checks that the selector does not find an element in the browser.

  $self = $self->live_element_exists("div.foo");

See L<Test::Mojo/element_exists_not>.

=head2 live_text_is

  $self = $self->live_text_is("div.name", "Mojo");

Checks text content of the CSS selectors first matching HTML element in the
browser matches the given string.

=head2 live_text_like

  $self = $self->live_text_is("div.name", qr{Mojo});

Checks text content of the CSS selectors first matching HTML element in the
browser matches the given regex.

=head2 live_value_is

  $self = $self->live_value_is("div.name", "Mojo");

Checks value of the CSS selectors first matching HTML element in the browser
matches the given string.

=head2 live_value_like

  $self = $self->live_value_like("div.name", qr{Mojo});

Checks value of the CSS selectors first matching HTML element in the browser
matches the given regex.

=head2 navigate_ok

  $self = $self->navigate_ok("/");
  $self = $self->navigate_ok("http://mojolicious.org/");

Open a browser window and go to the given location.

=head2 new

  $self = $class->new;
  $self = $class->new($app);

Same as L<Test::Mojo/new>, but will not build C<$app> if
L</MOJO_SELENIUM_BASE_URL> is set.

=head2 refresh

  $self = $self->refresh;

Equivalent to hitting the refresh button on the browser.

See L<Selenium::Remote::Driver/refresh>.

=head2 send_keys_ok

  $self->send_keys_ok("input[name=name]", ["web", \"space", "framework"]);
  $self->send_keys_ok(undef, [\"return"]);

Used to send keys to a given element. Scalar refs will be sent as
L<Selenium::Remote::WDKeys> strings. Passing in C<undef> as the first argument
will cause the keys to be sent to the currently active element.

List of some of the special keys:

=over 2

=item * alt, control, shift

=item * right_arrow, down_arrow, left_arrow, up_arrow

=item * backspace, clear, delete, enter, return, escape, space, tab

=item * f1, f2, ..., f12

=item * command_meta, pause

=back

=head2 set_window_size

  $self = $self->set_window_size([$width, $height]);
  $self = $self->set_window_size([375, 667]);

Set the browser window size.

=head2 setup_or_skip_all

  $self = $self->setup_or_skip_all;

Will L<skip all#Test::More/skip_all> tests unless C<TEST_SELENIUM> is set and
and L</driver> can be built.

Will also set L</MOJO_SELENIUM_BASE_URL> if C<TEST_SELENIUM> looks like a URL.

=head2 submit_ok

  $self = $self->submit_ok("form");

Submit a form, either by selector or the current active form.

See L<Selenium::Remote::WebElement/submit>.

=head2 toggle_checked_ok

  $self = $self->toggle_checked_ok("input[name=human]");

Used to toggle the "checked" attribute either with a click event or fallback to
javascript.

=head2 wait_for

  $self = $self->wait_for(0.2);
  $self = $self->wait_for('[name="agree"]', "test description");
  $self = $self->wait_for('[name="agree"]:enabled', {interval => 1.5, timeout => 10});
  $self = $self->wait_for('[name="agree"]:selected');
  $self = $self->wait_for('[href="/"]:visible');
  $self = $self->wait_for('[href="/hidden"]:hidden');
  $self = $self->wait_for('[name=checkbox]:checked');

Simpler version of L</wait_until> for the most common use cases:

=over 2

=item Number

Allows the browser and server to run for a given interval in seconds. This is
useful if you want the browser to receive data from the server or simply let
C<setTimeout()> in JavaScript run.

=item String

Wait for an element matching the CSS selector with some additional modifiers:
L<:enabled|Selenium::Remote::WebElement#is_enabled>,
L<:hidden|Selenium::Remote::WebElement#is_hidden>,
L<:selected|Selenium::Remote::WebElement#is_selected> and
L<:visible|Selenium::Remote::WebElement#is_displayed>.

Check out L<Selenium::Remote::WebElement> for details about the modifiers.

=back

=head2 wait_until

  $self = $self->wait_until(sub { my $self = shift; return 1 }, \%args);
  $self = $self->wait_until(sub { $_->get_current_url =~ /foo/ }, \%args);

  # Use it as a sleep(0.8)
  $self = $self->wait_until(sub { 0 }, {timeout => 0.8, skip => 1});

Start L<Mojo::IOLoop> and run it until the callback returns true. Note that
C<$_[0]> is C<$self> and C<$_> is L</driver>. C<%args> is optional, but can
contain these values:

  {
    interval => $seconds, # Default: 0.5
    timeout  => $seconds, # Default: 60
    skip     => $bool,    # Default: 0
  }

=head2 window_size_is

  $self = $self->window_size_is([$width, $height]);
  $self = $self->window_size_is([375, 667]);

Test if window has the expected width and height.

=head1 AUTHOR

Jan Henning Thorsen

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2014, Jan Henning Thorsen

This program is free software, you can redistribute it and/or modify it under
the terms of the Artistic License version 2.0.

=head1 SEE ALSO

L<Test::Mojo>.

L<Selenium::Remote::Driver>

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.