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.104;
2              
3 40     40   304367 use 5.020;
  40         423  
4 40     40   240 use feature qw/lexical_subs postderef signatures/;
  40         82  
  40         7269  
5 40     40   283 use warnings;
  40         104  
  40         1425  
6 40     40   253 no warnings 'experimental';
  40         102  
  40         6112  
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   51923 use overload fallback => 1, '&{}' => sub { $_[0][4] };
  40     6   46236  
  40         398  
  6         5513  
18              
19 40     40   3623 use Carp 1.25 ();
  40         1613  
  40         1084  
20 40     40   30760 use HTTP::Tiny;
  40         2222942  
  40         1795  
21 40     40   33400 use JSON::PP ();
  40         581882  
  40         1460  
22 40     40   23193 use WebDriver::Tiny::Elements;
  40         132  
  40         106493  
23              
24             our @CARP_NOT = 'WebDriver::Tiny::Elements';
25              
26             sub import {
27             # https://www.w3.org/TR/webdriver/#sendkeys
28 4     4   164 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         2396 require charnames;
61              
62 4         128951 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 178 sub new($class, %args) {
  2         6  
  2         6  
  2         4  
75             Carp::croak qq/$class - Missing required parameter "port"/
76 2 100       207 unless exists $args{port};
77              
78 1   50     9 $args{host} //= 'localhost';
79 1   50     6 $args{path} //= '';
80              
81             my $self = bless [
82             HTTP::Tiny->new,
83             "http://$args{host}:$args{port}$args{path}/session",
84 1   50     9 $args{base_url} // '',
85             ], $class;
86              
87             my $reply = $self->_req(
88             POST => '',
89             { capabilities => { alwaysMatch => $args{capabilities} // {} } },
90 1   50     128 );
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 83 sub html($self) { $self->_req( GET => '/source' ) }
  1         3  
  1         2  
  1         5  
110 1     1 1 86 sub title($self) { $self->_req( GET => '/title' ) }
  1         3  
  1         2  
  1         4  
111 1     1 1 97 sub url($self) { $self->_req( GET => '/url' ) }
  1         3  
  1         2  
  1         6  
112              
113 2     2 1 645 sub back($self) { $self->_req( POST => '/back' ); $self }
  2         6  
  2         3  
  2         12  
  2         5  
114 2     2 1 625 sub forward($self) { $self->_req( POST => '/forward' ); $self }
  2         6  
  2         4  
  2         10  
  2         6  
115 2     2 1 676 sub refresh($self) { $self->_req( POST => '/refresh' ); $self }
  2         11  
  2         4  
  2         12  
  2         7  
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 188 sub alert_accept($self) { $self->_req( POST => '/alert/accept' ); $self }
  2         6  
  2         12  
  2         14  
  2         11  
125 2     2 1 927 sub alert_dismiss($self) { $self->_req( POST => '/alert/dismiss' ); $self }
  2         6  
  2         4  
  2         9  
  2         6  
126              
127 1     1 1 91 sub alert_text($self) { $self->_req( GET => '/alert/text' ) }
  1         3  
  1         2  
  1         4  
128              
129             sub base_url {
130 8 100   8 1 643 if ( @_ == 2 ) {
131 4   100     22 $_[0][2] = $_[1] // '';
132              
133 4         21 return $_[0];
134             }
135              
136 4         47 $_[0][2];
137             }
138              
139             sub cookie {
140 4     4 1 2898 my ( $self, $name, $value, @args ) = @_;
141              
142             # GET /cookie/{name} isn't supported by ChromeDriver, so get all.
143 4 100       24 return $self->cookies->{$name} if @_ == 2;
144              
145 3         21 $self->_req( POST => '/cookie',
146             { cookie => { name => $name, value => $value, @args } } );
147              
148 3         15 $self;
149             }
150              
151 5     5 1 2911 sub cookie_delete($self, @cookies) {
  5         12  
  5         13  
  5         8  
152 5 100       19 if (@cookies) {
153 3         15 $self->_req( DELETE => "/cookie/$_" ) for @cookies;
154             }
155             else {
156 2         11 $self->_req( DELETE => '/cookie' );
157             }
158              
159 5         19 $self;
160             }
161              
162             sub cookies {
163 2   50 2 1 84 my @cookies = @{ $_[0]->_req( GET => '/cookie' ) // [] };
  2         10  
164              
165             # Map the incorrect key to the correct key.
166 2   0     7 $_->{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 5601 sub find($self, $selector, %args) {
  12         22  
  12         23  
  12         26  
  12         19  
173 12         33 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     64 my $method = $methods->{ $args{method} // '' } // 'css selector';
      100        
182              
183             # FIXME
184 12 50       42 my $drv = ref $self eq 'WebDriver::Tiny::Elements' ? $self->[0] : $self;
185              
186 12         20 my @ids;
187              
188 12   50     61 for ( 0 .. ( $args{tries} // 5 ) ) {
189 12         63 my $reply = $self->_req(
190             POST => '/elements',
191             { using => $method, value => "$selector" },
192             );
193              
194 12         58 @ids = map $_->{+ELEMENT_ID}, @$reply;
195              
196             @ids = grep {
197 0         0 $drv->_req( GET => "/element/$_/displayed" )
198 12 50       31 } @ids if $args{visible};
199              
200 12 50       44 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     33 if !@ids && !exists $args{dies} && !$args{dies};
      0        
207              
208 12 50       67 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 90 sub js { unshift @_, '/execute/sync'; goto $js }
  2         11  
220 1     1 1 88 sub js_async { unshift @_, '/execute/async'; goto $js }
  1         6  
221              
222 7     7 1 5963 sub get($self, $url) {
  7         16  
  7         14  
  7         12  
223 7 100       89 $self->_req(
224             POST => '/url',
225             { url => $url =~ m(^https?://) ? $url : $self->[2] . $url },
226             );
227              
228 7         20 $self;
229             }
230              
231 1     1 1 87 sub screenshot($self, $file = undef) {
  1         3  
  1         2  
  1         2  
232 1         550 require MIME::Base64;
233              
234 1         670 my $data = MIME::Base64::decode_base64(
235             $self->_req( GET => '/screenshot' )
236             );
237              
238 1 50       4 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 90 sub user_agent($self) { $js->( '/execute/sync', $self, 'return window.navigator.userAgent') }
  1         3  
  1         2  
  1         5  
250              
251 1     1 1 84 sub window($self) { $self->_req( GET => '/window' ) }
  1         3  
  1         2  
  1         4  
252 1     1 1 84 sub windows($self) { $self->_req( GET => '/window/handles' ) }
  1         3  
  1         2  
  1         4  
253              
254 2     2 1 659 sub window_close($self) { $self->_req( DELETE => '/window' ); $self }
  2         6  
  2         4  
  2         9  
  2         6  
255 1     1 1 85 sub window_fullscreen($self) { $self->_req( POST => '/window/fullscreen' ); $self }
  1         3  
  1         2  
  1         4  
  1         2  
256 2     2 1 621 sub window_maximize($self) { $self->_req( POST => '/window/maximize' ); $self }
  2         6  
  2         5  
  2         9  
  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 573 my $self = shift;
261              
262 1 50       5 return $self->_req( GET => '/window/rect' ) unless @_;
263              
264 1         6 $#_ = 3;
265              
266 1         2 my %args;
267 1   100     20 @args{ qw/width height x y/ } = map $_ // 0, @_;
268              
269 1         5 $self->_req( POST => '/window/rect', \%args );
270              
271 1         7 $self;
272             }
273              
274 2     2 1 754 sub window_switch($self, $handle) {
  2         6  
  2         5  
  2         3  
275 2         14 $self->_req( POST => '/window', { handle => $handle } );
276              
277 2         9 $self;
278             }
279              
280 73     73   145 sub _req($self, $method, $path, $args = undef) {
  73         124  
  73         138  
  73         128  
  73         145  
  73         113  
281 73   100     1650 my $reply = $self->[0]->request(
282             $method,
283             $self->[1] . $path,
284             { content => JSON::PP::encode_json( $args // {} ) },
285             );
286              
287 73         25641 my $value = eval { JSON::PP::decode_json( $reply->{content} )->{value} };
  73         301  
288              
289 73 100       10781 unless ( $reply->{success} ) {
290             my $error = $value
291             ? $value->{message} || $value->{error} || $reply->{content}
292 1 50 0     5 : $reply->{content};
293              
294 1         131 Carp::croak ref $self, ' - ', $error;
295             }
296              
297 72         258 $value;
298             }
299              
300 1 50 33 1   3 sub DESTROY($self) { $self->_req( DELETE => '' ) if $self->[0] && $self->[3] }
  1         2  
  1         2  
  1         18  
301              
302             1;