File Coverage

blib/lib/WebDriver/Tiny.pm
Criterion Covered Total %
statement 191 217 88.0
branch 20 34 58.8
condition 18 39 46.1
subroutine 40 44 90.9
pod 29 30 96.6
total 298 364 81.8


line stmt bran cond sub pod time code
1             package WebDriver::Tiny 0.103;
2              
3 40     40   298445 use 5.020;
  40         386  
4 40     40   234 use feature qw/lexical_subs postderef signatures/;
  40         79  
  40         6826  
5 40     40   280 use warnings;
  40         75  
  40         1371  
6 40     40   266 no warnings 'experimental';
  40         110  
  40         5713  
7              
8             # https://www.w3.org/TR/webdriver/#elements
9             my sub ELEMENT_ID :prototype() {'element-6066-11e4-a52e-4f735466cecf'}
10              
11             # Allow "cute" $drv->('selector') syntax.
12             #
13             # $self->[4] contains a sub ref that closes over a weakened $self that calls
14             # find, i.e. sub { $weak_self->find(@_) }. This sub ref is returned when $self
15             # is invoked as a sub ref thanks to the magic of overloading. We weaken to
16             # avoid a memory leak. The closure is built in new().
17 40     40   50447 use overload fallback => 1, '&{}' => sub { $_[0][4] };
  40     6   44968  
  40         363  
  6         4726  
18              
19 40     40   3404 use Carp 1.25 ();
  40         1263  
  40         1121  
20 40     40   29073 use HTTP::Tiny;
  40         2126772  
  40         1687  
21 40     40   31174 use JSON::PP ();
  40         567401  
  40         1376  
22 40     40   20459 use WebDriver::Tiny::Elements;
  40         117  
  40         104621  
23              
24             our @CARP_NOT = 'WebDriver::Tiny::Elements';
25              
26             sub import {
27             # https://www.w3.org/TR/webdriver/#sendkeys
28 4     4   149 state $chars = {
29             WD_NULL => 57344, WD_CANCEL => 57345,
30             WD_HELP => 57346, WD_BACK_SPACE => 57347,
31             WD_TAB => 57348, WD_CLEAR => 57349,
32             WD_RETURN => 57350, WD_ENTER => 57351,
33             WD_SHIFT => 57352, WD_CONTROL => 57353,
34             WD_ALT => 57354, WD_PAUSE => 57355,
35             WD_ESCAPE => 57356, WD_SPACE => 57357,
36             WD_PAGE_UP => 57358, WD_PAGE_DOWN => 57359,
37             WD_END => 57360, WD_HOME => 57361,
38             WD_ARROW_LEFT => 57362, WD_ARROW_UP => 57363,
39             WD_ARROW_RIGHT => 57364, WD_ARROW_DOWN => 57365,
40             WD_INSERT => 57366, WD_DELETE => 57367,
41             WD_SEMICOLON => 57368, WD_EQUALS => 57369,
42             WD_NUMPAD0 => 57370, WD_NUMPAD1 => 57371,
43             WD_NUMPAD2 => 57372, WD_NUMPAD3 => 57373,
44             WD_NUMPAD4 => 57374, WD_NUMPAD5 => 57375,
45             WD_NUMPAD6 => 57376, WD_NUMPAD7 => 57377,
46             WD_NUMPAD8 => 57378, WD_NUMPAD9 => 57379,
47             WD_MULTIPLY => 57380, WD_ADD => 57381,
48             WD_SEPARATOR => 57382, WD_SUBTRACT => 57383,
49             WD_DECIMAL => 57384, WD_DIVIDE => 57385,
50             WD_F1 => 57393, WD_F2 => 57394,
51             WD_F3 => 57395, WD_F4 => 57396,
52             WD_F5 => 57397, WD_F6 => 57398,
53             WD_F7 => 57399, WD_F8 => 57400,
54             WD_F9 => 57401, WD_F10 => 57402,
55             WD_F11 => 57403, WD_F12 => 57404,
56             WD_META => 57405, WD_COMMAND => 57405,
57             WD_ZENKAKU_HANKAKU => 57408,
58             };
59              
60 4         2288 require charnames;
61              
62 4         125917 charnames->import( ':alias' => $chars );
63             }
64              
65             # We're a blessed arrayref (for speed) of the following parts:
66             #
67             # $self = [
68             # 0: HTTP::Tiny instance,
69             # 1: URL of the WebDriver daemon,
70             # 2: Base URL which schemeless get calls are based off,
71             # 3: The capabilities of the WebDriver daemon,
72             # 4: Cached closure of $self for ->() syntax,
73             # ]
74 2     2 1 197 sub new($class, %args) {
  2         6  
  2         6  
  2         4  
75             Carp::croak qq/$class - Missing required parameter "port"/
76 2 100       224 unless exists $args{port};
77              
78 1   50     7 $args{host} //= 'localhost';
79 1   50     7 $args{path} //= '';
80              
81             my $self = bless [
82             HTTP::Tiny->new,
83             "http://$args{host}:$args{port}$args{path}/session",
84 1   50     5 $args{base_url} // '',
85             ], $class;
86              
87             my $reply = $self->_req(
88             POST => '',
89             { capabilities => { alwaysMatch => $args{capabilities} // {} } },
90 1   50     125 );
91              
92 0         0 $self->[1] .= '/' . $reply->{sessionId};
93              
94             # Store the capabilities.
95 0         0 $self->[3] = $reply->{capabilities};
96              
97             # Numify bool objects, saves memory.
98 0         0 $_ += 0 for grep ref eq 'JSON::PP::Boolean', values $self->[3]->%*;
99              
100             # See the overloading at the top of the file for details.
101 0         0 Scalar::Util::weaken( my $weak_self = $self );
102 0     0   0 $self->[4] = sub { $weak_self->find(@_) };
  0         0  
103              
104 0         0 $self;
105             }
106              
107 0     0 1 0 sub capabilities($self) { $self->[3] }
  0         0  
  0         0  
  0         0  
108              
109 1     1 1 84 sub html($self) { $self->_req( GET => '/source' ) }
  1         3  
  1         2  
  1         4  
110 1     1 1 84 sub title($self) { $self->_req( GET => '/title' ) }
  1         4  
  1         2  
  1         4  
111 1     1 1 87 sub url($self) { $self->_req( GET => '/url' ) }
  1         6  
  1         3  
  1         4  
112              
113 2     2 1 631 sub back($self) { $self->_req( POST => '/back' ); $self }
  2         5  
  2         4  
  2         8  
  2         7  
114 2     2 1 667 sub forward($self) { $self->_req( POST => '/forward' ); $self }
  2         6  
  2         5  
  2         11  
  2         7  
115 2     2 1 633 sub refresh($self) { $self->_req( POST => '/refresh' ); $self }
  2         5  
  2         4  
  2         11  
  2         5  
116              
117             sub status {
118             # /status is the only path without the session prefix, so surpress it.
119 0     0 1 0 local $_[0][1] = substr $_[0][1], 0, rindex $_[0][1], '/session/';
120              
121 0         0 $_[0]->_req( GET => '/status' );
122             }
123              
124 2     2 1 179 sub alert_accept($self) { $self->_req( POST => '/alert/accept' ); $self }
  2         5  
  2         13  
  2         13  
  2         9  
125 2     2 1 856 sub alert_dismiss($self) { $self->_req( POST => '/alert/dismiss' ); $self }
  2         5  
  2         3  
  2         8  
  2         7  
126              
127 1     1 1 85 sub alert_text($self) { $self->_req( GET => '/alert/text' ) }
  1         3  
  1         2  
  1         5  
128              
129             sub base_url {
130 8 100   8 1 717 if ( @_ == 2 ) {
131 4   100     21 $_[0][2] = $_[1] // '';
132              
133 4         20 return $_[0];
134             }
135              
136 4         43 $_[0][2];
137             }
138              
139             sub cookie {
140 4     4 1 2588 my ( $self, $name, $value, @args ) = @_;
141              
142             # GET /cookie/{name} isn't supported by ChromeDriver, so get all.
143 4 100       20 return $self->cookies->{$name} if @_ == 2;
144              
145 3         19 $self->_req( POST => '/cookie',
146             { cookie => { name => $name, value => $value, @args } } );
147              
148 3         14 $self;
149             }
150              
151 5     5 1 2893 sub cookie_delete($self, @cookies) {
  5         10  
  5         10  
  5         9  
152 5 100       16 if (@cookies) {
153 3         18 $self->_req( DELETE => "/cookie/$_" ) for @cookies;
154             }
155             else {
156 2         6 $self->_req( DELETE => '/cookie' );
157             }
158              
159 5         16 $self;
160             }
161              
162             sub cookies {
163 2   50 2 1 90 my @cookies = @{ $_[0]->_req( GET => '/cookie' ) // [] };
  2         13  
164              
165             # Map the incorrect key to the correct key.
166 2   0     6 $_->{httpOnly} //= delete $_->{httponly} for @cookies;
167              
168 2         9 +{ map { $_->{name} => $_ } @cookies };
  0         0  
169             }
170              
171             # NOTE This method can be called from a driver or a collection of elements.
172 12     12 1 4755 sub find($self, $selector, %args) {
  12         25  
  12         21  
  12         28  
  12         18  
173 12         35 state $methods = {
174             css => 'css selector',
175             ecmascript => 'ecmascript',
176             link_text => 'link text',
177             partial_link_text => 'partial link text',
178             xpath => 'xpath',
179             };
180              
181 12   100     59 my $method = $methods->{ $args{method} // '' } // 'css selector';
      100        
182              
183             # FIXME
184 12 50       39 my $drv = ref $self eq 'WebDriver::Tiny::Elements' ? $self->[0] : $self;
185              
186 12         18 my @ids;
187              
188 12   50     57 for ( 0 .. ( $args{tries} // 5 ) ) {
189 12         61 my $reply = $self->_req(
190             POST => '/elements',
191             { using => $method, value => "$selector" },
192             );
193              
194 12         55 @ids = map $_->{+ELEMENT_ID}, @$reply;
195              
196             @ids = grep {
197 0         0 $drv->_req( GET => "/element/$_/displayed" )
198 12 50       36 } @ids if $args{visible};
199              
200 12 50       45 last if @ids;
201              
202 0   0     0 select undef, undef, undef, $args{sleep} // .1;
203             }
204              
205             Carp::croak ref $self, qq/->find failed for $method = "$_[1]"/
206 12 0 33     30 if !@ids && !exists $args{dies} && !$args{dies};
      0        
207              
208 12 50       65 wantarray ? map { bless [ $drv, $_ ], 'WebDriver::Tiny::Elements' } @ids
  0         0  
209             : bless [ $drv, @ids ], 'WebDriver::Tiny::Elements';
210             }
211              
212             my $js = sub($path, $self, $script, @args) {
213             # Currently only takes the first ID in the collection, this should change.
214             $_ = { ELEMENT_ID, $_->[1] }
215             for grep ref eq 'WebDriver::Tiny::Elements', @args;
216             $self->_req( POST => $path, { script => $script, args => \@args } );
217             };
218              
219 2     2 1 113 sub js { unshift @_, '/execute/sync'; goto $js }
  2         22  
220 1     1 1 84 sub js_async { unshift @_, '/execute/async'; goto $js }
  1         6  
221              
222 7     7 1 5188 sub get($self, $url) {
  7         12  
  7         15  
  7         10  
223 7 100       90 $self->_req(
224             POST => '/url',
225             { url => $url =~ m(^https?://) ? $url : $self->[2] . $url },
226             );
227              
228 7         21 $self;
229             }
230              
231 1     1 1 88 sub screenshot($self, $file = undef) {
  1         3  
  1         2  
  1         2  
232 1         566 require MIME::Base64;
233              
234 1         631 my $data = MIME::Base64::decode_base64(
235             $self->_req( GET => '/screenshot' )
236             );
237              
238 1 50       5 if ( defined $file ) {
239 0 0       0 open my $fh, '>', $file or die $!;
240 0         0 print $fh $data;
241 0 0       0 close $fh or die $!;
242              
243 0         0 return $self;
244             }
245              
246 1         3 $data;
247             }
248              
249 1     1 1 83 sub user_agent($self) { $js->( '/execute/sync', $self, 'return window.navigator.userAgent') }
  1         3  
  1         2  
  1         4  
250              
251 1     1 1 90 sub window($self) { $self->_req( GET => '/window' ) }
  1         3  
  1         2  
  1         5  
252 1     1 1 83 sub windows($self) { $self->_req( GET => '/window/handles' ) }
  1         3  
  1         2  
  1         3  
253              
254 2     2 1 715 sub window_close($self) { $self->_req( DELETE => '/window' ); $self }
  2         6  
  2         4  
  2         8  
  2         5  
255 1     1 1 84 sub window_fullscreen($self) { $self->_req( POST => '/window/fullscreen' ); $self }
  1         3  
  1         3  
  1         4  
  1         3  
256 2     2 1 699 sub window_maximize($self) { $self->_req( POST => '/window/maximize' ); $self }
  2         4  
  2         4  
  2         10  
  2         6  
257 0     0 0 0 sub window_minimize($self) { $self->_req( POST => '/window/minimize' ); $self }
  0         0  
  0         0  
  0         0  
  0         0  
258              
259             sub window_rect {
260 1     1 1 617 my $self = shift;
261              
262 1 50       6 return $self->_req( GET => '/window/rect' ) unless @_;
263              
264 1         5 $#_ = 3;
265              
266 1         2 my %args;
267 1   100     20 @args{ qw/width height x y/ } = map $_ // 0, @_;
268              
269 1         6 $self->_req( POST => '/window/rect', \%args );
270              
271 1         6 $self;
272             }
273              
274 2     2 1 639 sub window_switch($self, $handle) {
  2         7  
  2         4  
  2         2  
275 2         14 $self->_req( POST => '/window', { handle => $handle } );
276              
277 2         8 $self;
278             }
279              
280 73     73   139 sub _req($self, $method, $path, $args = undef) {
  73         125  
  73         131  
  73         116  
  73         153  
  73         105  
281 73   100     1609 my $reply = $self->[0]->request(
282             $method,
283             $self->[1] . $path,
284             { content => JSON::PP::encode_json( $args // {} ) },
285             );
286              
287 73         25500 my $value = eval { JSON::PP::decode_json( $reply->{content} )->{value} };
  73         269  
288              
289 73 100       10663 unless ( $reply->{success} ) {
290             my $error = $value
291             ? $value->{message} || $value->{error} || $reply->{content}
292 1 50 0     6 : $reply->{content};
293              
294 1         121 Carp::croak ref $self, ' - ', $error;
295             }
296              
297 72         247 $value;
298             }
299              
300 1 50 33 1   3 sub DESTROY($self) { $self->_req( DELETE => '' ) if $self->[0] && $self->[3] }
  1         3  
  1         2  
  1         17  
301              
302             1;