-
Notifications
You must be signed in to change notification settings - Fork 91
/
Driver.pm
3488 lines (2665 loc) · 98.8 KB
/
Driver.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package Selenium::Remote::Driver;
use strict;
use warnings;
# ABSTRACT: Perl Client for Selenium Remote Driver
use Moo;
use Try::Tiny;
use 5.006;
use v5.10.0; # Before 5.006, v5.10.0 would not be understood.
# See http://perldoc.perl.org/5.10.0/functions/use.html#use-VERSION
# and http://www.dagolden.com/index.php/369/version-numbers-should-be-boring/
# for details.
use Carp;
our @CARP_NOT;
use IO::String;
use Archive::Zip qw( :ERROR_CODES );
use Scalar::Util;
use Selenium::Remote::RemoteConnection;
use Selenium::Remote::Commands;
use Selenium::Remote::Spec;
use Selenium::Remote::WebElement;
use Selenium::Remote::WDKeys;
use File::Spec::Functions ();
use File::Basename qw(basename);
use Sub::Install ();
use MIME::Base64 ();
use Time::HiRes qw(usleep);
use Clone qw{clone};
use List::Util qw{any};
use constant FINDERS => {
class => 'class name',
class_name => 'class name',
css => 'css selector',
id => 'id',
link => 'link text',
link_text => 'link text',
name => 'name',
partial_link_text => 'partial link text',
tag_name => 'tag name',
xpath => 'xpath',
};
our $FORCE_WD2 = 0;
our %CURRENT_ACTION_CHAIN = ( actions => [] );
=for Pod::Coverage BUILD
=for Pod::Coverage DEMOLISH
=head1 SYNOPSIS
use Selenium::Remote::Driver;
my $driver = Selenium::Remote::Driver->new;
$driver->get('http://www.google.com');
print $driver->get_title();
$driver->quit();
=cut
=head1 DESCRIPTION
Selenium is a test tool that allows you to write
automated web application UI tests in any programming language against
any HTTP website using any mainstream JavaScript-enabled browser. This module is
an implementation of the client for the Remote driver that Selenium provides.
You can find bindings for other languages at this location:
L<http://code.google.com/p/selenium/>
This module sends commands directly to the Server using HTTP. Using this module
together with the Selenium Server, you can automatically control any supported
browser. To use this module, you need to have already downloaded and started
the Selenium Server (Selenium Server is a Java application).
=cut
=head1 USAGE
=head2 Without Standalone Server
As of v0.25, it's possible to use this module without a standalone
server - that is, you would not need the JRE or the JDK to run your
Selenium tests. See L<Selenium::Chrome>, L<Selenium::PhantomJS>, and
L<Selenium::Firefox> for details. If you'd like additional browsers
besides these, give us a holler over in
L<Github|https://github.com/teodesian/Selenium-Remote-Driver/issues>.
=head2 Remote Driver Response
Selenium::Remote::Driver uses the
L<JsonWireProtocol|http://code.google.com/p/selenium/wiki/JsonWireProtocol>
And the
L<WC3 WebDriver Protocol|https://www.w3.org/TR/webdriver/>
to communicate with the Selenium Server. If an error occurs while
executing the command then the server sends back an HTTP error code
with a JSON encoded reponse that indicates the precise
L<Response Error Code|http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes>.
The module will then croak with the error message associated with this
code. If no error occurred, then the subroutine called will return the
value sent back from the server (if a return value was sent).
So a rule of thumb while invoking methods on the driver is if the method did not
croak when called, then you can safely assume the command was successful even if
nothing was returned by the method.
=head2 WebElement
Selenium Webdriver represents all the HTML elements as WebElement, which is
in turn represented by L<Selenium::Remote::WebElement> module. So any method that
deals with WebElements will return and/or expect WebElement object. The POD for
that module describes all the methods that perform various actions on the
WebElements like click, submit etc.
To interact with any WebElement you have to first "find" it, read the POD for
find_element or find_elements for further info. Once you find the required element
then you can perform various actions. If you don't call find_* method first, all
your further actions will fail for that element. Finally, just remember that you
don't have to instantiate WebElement objects at all - they will be automatically
created when you use the find_* methods.
A sub-class of Selenium::Remote::WebElement may be used instead of Selenium::Remote::WebElement,
by providing that class name as an option the constructor:
my $driver = Selenium::Remote::Driver->new( webelement_class => ... );
For example, a testing-subclass may extend the web-element object with testing methods.
=head2 LWP Read Timeout errors
It's possible to make Selenium calls that take longer than the default
L<LWP::UserAgent> timeout. For example, setting the asynchronous
script timeout greater than the LWP::UserAgent timeout and then
executing a long running asynchronous snippet of javascript will
immediately trigger an error like:
Error while executing command: executeAsyncScript: Server returned
error message read timeout at...
You can get around this by configuring LWP's timeout value, either by
constructing your own LWP and passing it in to ::Driver during
instantiation
my $timeout_ua = LWP::UserAgent->new;
$timeout_ua->timeout(360); # this value is in seconds!
my $d = Selenium::Remote::Driver->new( ua => $timeout_ua );
or by configuring the timeout on the fly as necessary:
use feature qw/say/;
use Selenium::Remote::Driver;
my $d = Selenium::Remote::Driver->new;
say $d->ua->timeout; # 180 seconds is the default
$d->ua->timeout(2); # LWP wants seconds, not milliseconds!
$d->set_timeout('script', 1000); # S::R::D wants milliseconds!
# Async scripts only return when the callback is invoked. Since there
# is no callback here, Selenium will block for the entire duration of
# the async timeout script. This will hit Selenium's async script
# timeout before hitting LWP::UserAgent's read timeout
$d->execute_async_script('return "hello"');
$d->quit;
=head1 TESTING
If are writing automated tests using this module, you may be
interested in L<Test::Selenium::Remote::Driver> which is also included
in this distribution. It includes convenience testing methods for many
of the selenum methods available here.
Your other option is to use this module in conjunction with your
choice of testing modules, like L<Test::Spec> or L<Test::More> as
you please.
=head1 WC3 WEBDRIVER COMPATIBILITY
WC3 Webdriver is a constantly evolving standard, so some things may or may not work at any given time.
That said, the following 'sanity tests' in the at/ (acceptance test) directory of the module passed on the following versions:
=over 4
=item Selenium Server: 3.8.1 - all tests
=item geckodriver: 0.19.1 - at/sanity.test
=item chromedriver: 2.35 - at/sanity-chrome.test
=item edgedriver: 5.16299 - at/sanity-edge.test
=item InternetExplorerDriver : 3.8.1 - at/sanity-ie.test (be sure to enable 'allow local files to run active content in your 'advanced settings' pane)
=item safaridriver : 11.0.2 - at/sanity-safari.test (be sure to enable 'allow automated testing' in the developer menu) -- it appears WC3 spec is *unimplemented*
=back
These tests are intended to be run directly against a working selenium server on the local host with said drivers configured.
If you are curious as to what 'works and does not' on your driver versions (and a few other quirks),
it is strongly encouraged you look at where the test calls the methods you are interested in.
While other browsers/drivers (especially legacy ones) likely work fine as well,
any new browser/driver will likely have problems if it's not listed above.
There is also a 'legacy.test' file available to run against old browsers/selenium (2.x servers, pre geckodriver).
This should only be used to verify backwards-compatibility has not been broken.
=head1 CONSTRUCTOR
=head2 new
Dies if communication with the selenium server cannot be established.
Input: (all optional)
Desired capabilities - HASH - Following options are accepted:
=over 4
=item B<remote_server_addr> - <string> - IP or FQDN of the Webdriver server machine. Default: 'localhost'
=item B<port> - <string> - Port on which the Webdriver server is listening. Default: 4444
=item B<browser_name> - <string> - desired browser string: {phantomjs|firefox|internet explorer|MicrosoftEdge|safari|htmlunit|iphone|chrome}
=item B<version> - <string> - desired browser version number
=item B<platform> - <string> - desired platform: {WINDOWS|XP|VISTA|MAC|LINUX|UNIX|ANY}
=item B<accept_ssl_certs> - <boolean> - whether SSL certs should be accepted, default is true.
=item B<firefox_profile> - Profile - Use Selenium::Firefox::Profile to create a Firefox profile for the browser to use. Optionally can pass a base64'd zip data of a profile directory if you don't like Selenium::Firefox::Profile.
=item B<proxy> - HASH - Proxy configuration with the following keys:
=item B<javascript> - <boolean> - Whether or not to use Javascript. You probably won't disable this, as you would be using L<WWW::Mechanize> instead. Default: True
=item B<auto_close> - <boolean> - Whether to automatically close the browser session on the server when the object goes out of scope. Default: False.
=item B<default_finder> - <string> - Default method by which to evaluate selectors. Default: 'xpath'
=item B<session_id> - <string> - Provide a Session ID to highjack a browser session on the remote server. Useful for micro-optimizers. Default: undef
=over 4
=item B<proxyType> - <string> - REQUIRED, Possible values are:
direct - A direct connection - no proxy in use,
manual - Manual proxy settings configured, e.g. setting a proxy for HTTP, a proxy for FTP, etc,
pac - Proxy autoconfiguration from a URL,
autodetect - proxy autodetection, probably with WPAD,
system - Use system settings
=item B<proxyAutoconfigUrl> - <string> - REQUIRED if proxyType is 'pac', ignored otherwise. Expected format: http://hostname.com:1234/pacfile or file:///path/to/pacfile
=item B<ftpProxy> - <string> - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234
=item B<httpProxy> - <string> - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234
=item B<sslProxy> - <string> - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234
=item B<socksProxy> - <string> - OPTIONAL, ignored if proxyType is not 'manual'. Expected format: hostname.com:1234. WebDriver 3 only.
=item B<socksVersion> - <int> - OPTIONAL, ignored if proxyType is not 'manual'. WebDriver 3 only.
=item B<noProxy> - <ARRAY> - OPTIONAL, list of URLs to bypass the proxy for. WebDriver3 only.
=back
=item B<pageLoadStrategy> - STRING - OPTIONAL, 'normal|eager|none'. default 'normal'. WebDriver3 only.
=item B<extra_capabilities> - HASH - Any other extra capabilities. Accepted keys will vary by browser. If firefox_profile is passed, the args (or profile) key will be overwritten, depending on how it was passed.
=back
On WebDriver3 the 'extra_capabilities' will be automatically converted into the parameter needed by your browser.
For example, extra_capabilities is passed to the server as the moz:firefoxOptions parameter.
You can also specify some options in the constructor hash that are
not part of the browser-related desired capabilities.
=over 4
=item B<auto_close> - <boolean> - whether driver should end session on remote server on close.
=item B<base_url> - <string> - OPTIONAL, base url for the website Selenium acts on. This can save you from repeating the domain in every call to $driver->get()
=item B<default_finder> - <string> - choose default finder used for find_element* {class|class_name|css|id|link|link_text|name|partial_link_text|tag_name|xpath}
=item B<inner_window_size> - <aref[Int]> - An array ref [ height, width ] that the browser window should use as its initial size immediately after instantiation
=item B<error_handler> - CODEREF - A CODEREF that we will call in event of any exceptions. See L</error_handler> for more details.
=item B<webelement_class> - <string> - sub-class of Selenium::Remote::WebElement if you wish to use an alternate WebElement class.
=item B<ua> - LWP::UserAgent instance - if you wish to use a specific $ua, like from Test::LWP::UserAgent
=back
Output:
Remote Driver object
Usage:
my $driver = Selenium::Remote::Driver->new;
or
my $driver = Selenium::Remote::Driver->new('browser_name' => 'firefox',
'platform' => 'MAC');
or (for Firefox 47 or lower on Selenium 3+)
my $driver = Selenium::Remote::Driver->new('browser_name' => 'firefox',
'platform' => 'MAC',
'extra_capabilities' => {
'marionette' => \0,
});
or
my $driver = Selenium::Remote::Driver->new('remote_server_addr' => '10.10.1.1',
'port' => '2222',
'auto_close' => 0);
or
my $driver = Selenium::Remote::Driver->new('browser_name' =>'chrome',
'extra_capabilities' => {
'chromeOptions' => {
'args' => [
'window-size=1260,960',
'incognito'
],
'prefs' => {
'session' => {
'restore_on_startup' => 4,
'urls_to_restore_on_startup' => [
'http://www.google.com',
'http://docs.seleniumhq.org'
]},
'first_run_tabs' => [
'http://www.google.com',
'http://docs.seleniumhq.org'
]
}
}
});
or
my $driver = Selenium::Remote::Driver->new('proxy' => {'proxyType' => 'manual', 'httpProxy' => 'myproxy.com:1234'});
or
my $driver = Selenium::Remote::Driver->new('default_finder' => 'css');
=head3 error_handler
=head3 clear_error_handler
OPTIONAL constructor arg & associated setter/clearer: if you wish to
install your own error handler, you may pass a code ref in to
C<error_handler> during instantiation like follows:
my $driver = Selenium::Remote::Driver->new(
error_handler => sub { print $_[1]; croak 'goodbye'; }
);
Additionally, you can set and/or clear it at any time on an
already-instantiated driver:
# later, change the error handler to something else
$driver->error_handler( sub { print $_[1]; croak 'hello'; } );
# stop handling errors manually and use the default S:R:D behavior
# (we will croak about the exception)
$driver->clear_error_handler;
Your error handler will receive two arguments: the first argument is
the C<$driver> object itself, and the second argument is the exception
message and stack trace in one multiline string.
B<N.B.>: If you set your own error handler, you are entirely
responsible for handling webdriver exceptions, _including_ croaking
behavior. That is, when you set an error handler, we will no longer
croak on Webdriver exceptions - it's up to you to do so. For
consistency with the standard S:R:D behavior, we recommend your error
handler also croak when it's done, especially since your test
shouldn't be running into unexpected errors. Catching specific or
desired errors in your error handler makes sense, but not croaking at
all can leave you in unfamiliar territory. Reaching an unexpected
exception might mean your test has gone off the rails, and the further
your test gets from the source of the of the exception, the harder it
will be to debug.
B<N.B.>: Four methods will still croak on their own: L</find_element>,
L</find_elements>, L</find_child_element>, and
L</find_child_elements>. If these methods throw a Webdriver Exception,
your error handler _will still be_ invoked inside an C<eval>, and then
they'll croak with their own error message that indicates the locator
and strategy used. So, your strategies for avoiding exceptions when
finding elements do not change (either use find_elements and check
the returned array size, wrap your calls to find_element* in an
C<eval>, or use the parameterized versions find_element_*).
=head2 new_from_caps
Description:
For experienced users who want complete control over the desired
capabilities, use this alternative constructor along with the
C<desired_capabilities> hash key in the init hash. Unlike "new",
this constructor will not assume any defaults for your desired
capabilities.
This alternate constructor IGNORES all other browser-related
desiredCapability options; the only options that will be respected
are those that are NOT part of the Capabilities JSON Object as
described in the Json Wire Protocol.
Input:
The only respected keys in the input hash are:
desired_capabilities - HASHREF - defaults to {}
remote_server_addr - STRING - defaults to localhost
port - INTEGER - defaults to 4444
default_finder - STRING - defaults to xpath
webelement_class - STRING - defaults to Selenium::Remote::WebElement
auto_close - BOOLEAN - defaults to 1
error_handler - CODEREF - defaults to croaking on exceptions
Except for C<desired_capabilities>, these keys perform exactly the
same as listed in the regular "new" constructor.
The hashref you pass in as desired_capabilities only gets json
encoded before being passed to the Selenium server; no default
options of any sort will be added.
This means you must handle normalization and casing of the input
options (like "browser_name" vs "browserName") and take care of
things like encoding the firefox profile if applicable. More
information about the desired capabilities object is available on
the Selenium wiki:
https://code.google.com/p/selenium/wiki/JsonWireProtocol#Capabilities_JSON_Object
Output:
Remote Driver object
Usage:
my $driver = Selenium::Remote::Driver->new_from_caps(
'desired_capabilities' => {'browserName' => 'firefox'}
);
The above would generate a POST to the webdriver server at
localhost:4444 with the exact payload of '{"desiredCapabilities":
{"browserName": "firefox" }}'.
=for Pod::Coverage has_base_url
=for Pod::Coverage has_desired_capabilities
=for Pod::Coverage has_error_handler
=for Pod::Coverage has_firefox_profile
=for Pod::Coverage has_inner_window_size
=for Pod::Coverage has_javascript
=for Pod::Coverage has_port
=for Pod::Coverage has_remote_server_addr
=cut
has 'remote_server_addr' => (
is => 'rw',
coerce => sub { ( defined($_[0]) ? $_[0] : 'localhost' )},
default => sub {'localhost'},
predicate => 1
);
has 'browser_name' => (
is => 'rw',
coerce => sub { ( defined($_[0]) ? $_[0] : 'firefox' )},
default => sub {'firefox'},
);
has 'base_url' => (
is => 'lazy',
coerce => sub {
my $base_url = shift;
$base_url =~ s|/$||;
return $base_url;
},
predicate => 'has_base_url',
);
has 'platform' => (
is => 'rw',
coerce => sub { ( defined($_[0]) ? $_[0] : 'ANY' )},
default => sub {'ANY'},
);
has 'port' => (
is => 'rw',
coerce => sub { ( defined($_[0]) ? $_[0] : '4444' )},
default => sub {'4444'},
predicate => 1
);
has 'version' => (
is => 'rw',
default => sub {''},
);
has 'webelement_class' => (
is => 'rw',
default => sub {'Selenium::Remote::WebElement'},
);
has 'default_finder' => (
is => 'rw',
coerce => sub { __PACKAGE__->FINDERS->{ $_[0] } },
default => sub {'xpath'},
);
has 'session_id' => (
is => 'rw',
default => sub {undef},
);
has 'remote_conn' => (
is => 'lazy',
builder => sub {
my $self = shift;
return Selenium::Remote::RemoteConnection->new(
remote_server_addr => $self->remote_server_addr,
port => $self->port,
ua => $self->ua,
wd_context_prefix => $self->wd_context_prefix
);
},
);
has 'error_handler' => (
is => 'rw',
coerce => sub {
my ($maybe_coderef) = @_;
if ( ref($maybe_coderef) eq 'CODE' ) {
return $maybe_coderef;
}
else {
croak 'The error handler must be a code ref.';
}
},
clearer => 1,
predicate => 1
);
has 'ua' => (
is => 'lazy',
builder => sub { return LWP::UserAgent->new }
);
has 'commands' => (
is => 'lazy',
builder => sub {
return Selenium::Remote::Commands->new;
},
);
has 'commands_v3' => (
is => 'lazy',
builder => sub {
return Selenium::Remote::Spec->new;
},
);
has 'auto_close' => (
is => 'rw',
coerce => sub { ( defined($_[0]) ? $_[0] : 1 )},
default => sub {1},
);
has 'pid' => (
is => 'lazy',
builder => sub { return $$ }
);
has 'javascript' => (
is => 'rw',
coerce => sub { $_[0] ? JSON::true : JSON::false },
default => sub { return JSON::true }
);
has 'accept_ssl_certs' => (
is => 'rw',
coerce => sub { $_[0] ? JSON::true : JSON::false },
default => sub { return JSON::true }
);
has 'proxy' => (
is => 'rw',
coerce => sub {
my $proxy = $_[0];
if ( $proxy->{proxyType} =~ /^pac$/i ) {
if ( not defined $proxy->{proxyAutoconfigUrl} ) {
croak "proxyAutoconfigUrl not provided\n";
}
elsif ( not( $proxy->{proxyAutoconfigUrl} =~ /^(http|file)/g ) ) {
croak "proxyAutoconfigUrl should be of format http:// or file://";
}
if ( $proxy->{proxyAutoconfigUrl} =~ /^file/ ) {
my $pac_url = $proxy->{proxyAutoconfigUrl};
my $file = $pac_url;
$file =~ s{^file://}{};
if (! -e $file) {
warn "proxyAutoConfigUrl file does not exist: '$pac_url'";
}
}
}
$proxy;
},
);
has 'extra_capabilities' => (
is => 'rw',
default => sub { {} }
);
has 'firefox_profile' => (
is => 'rw',
coerce => sub {
my $profile = shift;
unless (Scalar::Util::blessed($profile)
&& $profile->isa('Selenium::Firefox::Profile')) {
croak "firefox_profile should be a Selenium::Firefox::Profile\n";
}
return $profile;
},
predicate => 'has_firefox_profile',
clearer => 1
);
has 'desired_capabilities' => (
is => 'lazy',
predicate => 'has_desired_capabilities'
);
has 'inner_window_size' => (
is => 'lazy',
predicate => 1,
coerce => sub {
my $size = shift;
croak "inner_window_size must have two elements: [ height, width ]"
unless scalar @$size == 2;
foreach my $dim (@$size) {
croak 'inner_window_size only accepts integers, not: ' . $dim
unless Scalar::Util::looks_like_number($dim);
}
return $size;
},
);
# At the time of writing, Geckodriver uses a different endpoint than
# the java bindings for executing synchronous and asynchronous
# scripts. As a matter of fact, Geckodriver does conform to the W3C
# spec, but as are bound to support both while the java bindings
# transition to full spec support, we need some way to handle the
# difference.
has '_execute_script_suffix' => (
is => 'lazy',
default => ''
);
with 'Selenium::Remote::Finders';
with 'Selenium::Remote::Driver::CanSetWebdriverContext';
sub BUILD {
my $self = shift;
if ( !( defined $self->session_id ) ) {
if ($self->has_desired_capabilities) {
$self->new_desired_session( $self->desired_capabilities );
}
else {
# Connect to remote server & establish a new session
$self->new_session( $self->extra_capabilities );
}
}
if ( !( defined $self->session_id ) ) {
croak "Could not establish a session with the remote server\n";
}
elsif ($self->has_inner_window_size) {
my $size = $self->inner_window_size;
$self->set_inner_window_size(@$size);
}
# Setup non-croaking, parameter versions of finders
foreach my $by (keys %{ $self->FINDERS }) {
my $finder_name = 'find_element_by_' . $by;
# In case we get instantiated multiple times, we don't want to
# install into the name space every time.
unless ($self->can($finder_name)) {
my $find_sub = $self->_build_find_by($by);
Sub::Install::install_sub({
code => $find_sub,
into => __PACKAGE__,
as => $finder_name,
});
}
}
}
sub new_from_caps {
my ($self, %args) = @_;
if (not exists $args{desired_capabilities}) {
$args{desired_capabilities} = {};
}
return $self->new(%args);
}
sub DEMOLISH {
my ($self, $in_global_destruction) = @_;
return if $$ != $self->pid;
return if $in_global_destruction;
$self->quit() if ( $self->auto_close && defined $self->session_id );
}
# We install an 'around' because we can catch more exceptions this way
# than simply wrapping the explicit croaks in _execute_command.
around '_execute_command' => sub {
my $orig = shift;
my $self = shift;
# copy @_ because it gets lost in the way
my @args = @_;
my $return_value;
try {
$return_value = $orig->($self,@args);
}
catch {
if ($self->has_error_handler) {
$self->error_handler->($self,$_);
}
else {
croak $_;
}
};
return $return_value;
};
# This is an internal method used the Driver & is not supposed to be used by
# end user. This method is used by Driver to set up all the parameters
# (url & JSON), send commands & receive processed response from the server.
sub _execute_command {
my ( $self, $res, $params ) = @_;
$res->{'session_id'} = $self->session_id;
print "Prepping $res->{command}\n" if $self->{debug};
#webdriver 3 shims
return $self->{capabilities} if $res->{command} eq 'getCapabilities' && $self->{capabilities};
$res->{ms} = $params->{ms} if $params->{ms};
$res->{type} = $params->{type} if $params->{type};
$res->{text} = $params->{text} if $params->{text};
$res->{using} = $params->{using} if $params->{using};
$res->{value} = $params->{value} if $params->{value};
print "Executing $res->{command}\n" if $self->{debug};
my $resource = $self->{is_wd3} ? $self->commands_v3->get_params($res) : $self->commands->get_params($res);
#Fall-back to legacy if wd3 command doesn't exist
if (!$resource && $self->{is_wd3}) {
print "Falling back to legacy selenium method for $res->{command}\n" if $self->{debug};
$resource = $self->commands->get_params($res);
}
#XXX InternetExplorerDriver quirks
if ($self->{is_wd3} && $self->browser_name eq 'internet explorer') {
delete $params->{ms};
delete $params->{type};
delete $resource->{payload}->{type};
my $oldvalue = delete $params->{'page load'};
$params->{pageLoad} = $oldvalue if $oldvalue;
}
if ($resource) {
$params = {} unless $params;
my $resp = $self->remote_conn->request( $resource, $params);
#In general, the parse_response for v3 is better, which is why we use it *even if* we are falling back.
return $self->commands_v3->parse_response($res,$resp) if $self->{is_wd3};
return $self->commands->parse_response($res,$resp);
}
else {
croak "Couldn't retrieve command settings properly\n";
}
}
=head1 METHODS
=head2 new_session (extra_capabilities)
Make a new session on the server.
Called by new(), not intended for regular use.
Occaisonally handy for recovering from brower crashes.
DANGER DANGER DANGER
This will throw away your old session if you have not closed it!
DANGER DANGER DANGER
=cut
sub new_session {
my ( $self, $extra_capabilities ) = @_;
$extra_capabilities ||= {};
my $args = {
'desiredCapabilities' => {
'browserName' => $self->browser_name,
'platform' => $self->platform,
'javascriptEnabled' => $self->javascript,
'version' => $self->version,
'acceptSslCerts' => $self->accept_ssl_certs,
%$extra_capabilities,
},
};
if ( defined $self->proxy ) {
$args->{desiredCapabilities}->{proxy} = $self->proxy;
}
if ($args->{desiredCapabilities}->{browserName} =~ /firefox/i
&& $self->has_firefox_profile) {
$args->{desiredCapabilities}->{firefox_profile} = $self->firefox_profile->_encode;
}
$self->_request_new_session($args);
}
=head2 new_desired_session(capabilities)
Basically the same as new_session, but with caps.
Sort of an analog to new_from_caps.
=cut
sub new_desired_session {
my ( $self, $caps ) = @_;
$self->_request_new_session({
desiredCapabilities => $caps
});
}
sub _request_new_session {
my ( $self, $args ) = @_;
#XXX UGLY shim for webdriver3
$args->{capabilities}->{alwaysMatch} = clone($args->{desiredCapabilities});
my $cmap = $self->commands_v3->get_caps_map();
my $caps = $self->commands_v3->get_caps();
foreach my $cap (keys(%{$args->{capabilities}->{alwaysMatch} })) {
#Handle browser specific capabilities
if (exists($args->{desiredCapabilities}->{browserName}) && $cap eq 'extra_capabilities') {
if (exists $args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'}->{args}) {
$args->{capabilities}->{alwaysMatch}->{$cap}->{args} = $args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'}->{args};
}
$args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'} = $args->{capabilities}->{alwaysMatch}->{$cap} if $args->{desiredCapabilities}->{browserName} eq 'firefox';
$args->{capabilities}->{alwaysMatch}->{'chromeOptions'} = $args->{capabilities}->{alwaysMatch}->{$cap} if $args->{desiredCapabilities}->{browserName} eq 'chrome';
#Does not appear there are any MSIE based options, so let's just let that be
}
if (exists($args->{desiredCapabilities}->{browserName}) && $args->{desiredCapabilities}->{browserName} eq 'firefox' && $cap eq 'firefox_profile') {
if (ref $args->{capabilities}->{alwaysMatch}->{$cap} eq 'Selenium::Firefox::Profile') {
#XXX not sure if I need to keep a ref to the File::Temp::Tempdir object to prevent reaping
$args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'}->{args} = ['-profile', $args->{capabilities}->{alwaysMatch}->{$cap}->{profile_dir}->dirname()];
} else {
#previously undocumented feature that we can pass the encoded profile
$args->{capabilities}->{alwaysMatch}->{'moz:firefoxOptions'}->{profile} = $args->{capabilities}->{alwaysMatch}->{$cap};
}
}
foreach my $newkey (keys(%$cmap)) {
if ($newkey eq $cap) {
last if $cmap->{$newkey} eq $cap;
$args->{capabilities}->{alwaysMatch}->{$cmap->{$newkey}} = $args->{capabilities}->{alwaysMatch}->{$cap};
delete $args->{capabilities}->{alwaysMatch}->{$cap};
last;
}
}
delete $args->{capabilities}->{alwaysMatch}->{$cap} if !any { $_ eq $cap } @$caps;
}
delete $args->{capabilities} if $FORCE_WD2; #XXX 'secret' feature to help the legacy unit tests to work
# geckodriver has not yet implemented the GET /status endpoint
# https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver/status
if (! $self->isa('Selenium::Firefox')) {
$self->remote_conn->check_status();
}
# command => 'newSession' to fool the tests of commands implemented
# TODO: rewrite the testing better, this is so fragile.
my $resource_new_session = {
method => $self->commands->get_method('newSession'),
url => $self->commands->get_url('newSession'),
no_content_success => $self->commands->get_no_content_success('newSession'),
};
my $rc = $self->remote_conn;
my $resp = $rc->request(
$resource_new_session,
$args,
);
if ( ( defined $resp->{'sessionId'} ) && $resp->{'sessionId'} ne '' ) {
$self->session_id( $resp->{'sessionId'} );
}
else {
my $error = 'Could not create new session';
if (ref $resp->{cmd_return} eq 'HASH') {
$error .= ': ' . $resp->{cmd_return}->{message};
}
else {
$error .= ': ' . $resp->{cmd_return};
}
croak $error;
}
#Webdriver 3 - best guess that this is 'whats goin on'
if ( ref $resp->{cmd_return} eq 'HASH' && $resp->{cmd_return}->{capabilities}) {
$self->{is_wd3} = 1;
$self->{capabilities} = $resp->{cmd_return}->{capabilities};
}
#XXX chromedriver DOES NOT FOLLOW SPEC!
if ( ref $resp->{cmd_return} eq 'HASH' && $resp->{cmd_return}->{chrome}) {
if (defined $resp->{cmd_return}->{setWindowRect}) { #XXX i'm inferring we are wd3 based on the presence of this
$self->{is_wd3} = 1;
$self->{capabilities} = $resp->{cmd_return};
}
}
#XXX unsurprisingly, neither does microsoft
if ( ref $resp->{cmd_return} eq 'HASH' && $resp->{cmd_return}->{pageLoadStrategy} && $self->browser_name eq 'MicrosoftEdge') {
$self->{is_wd3} = 1;
$self->{capabilities} = $resp->{cmd_return};
}
return ($args,$resp);
}
=head2 is_webdriver_3
Print whether the server (or browser) thinks it's implemented webdriver 3.
If this returns true, webdriver 3 methods will be used in the case an action exists in L<Selenium::Remote::Spec> for the method you are trying to call.
If a method you are calling has no webdriver 3 equivalent (or browser extension), the legacy commands implemented in L<Selenium::Remote::Commands> will be used.
Note how I said *thinks* above. In the case you want to force usage of legacy methods, call set_webdriver_3() to work around various browser issues.
=cut
sub is_webdriver_3 {
my $self = shift;
return $self->{is_wd3};
}
=head2 debug_on
Description:
Turns on debugging mode and the driver will print extra info like request
and response to stdout. Useful, when you want to see what is being sent to
the server & what response you are getting back.
Usage:
$driver->debug_on;
=cut
sub debug_on {
my ($self) = @_;
$self->{debug} = 1;
$self->remote_conn->debug(1);
}
=head2 debug_off
Description: