File Coverage

blib/lib/WWW/Mechanize/Chrome.pm
Criterion Covered Total %
statement 240 2530 9.4
branch 39 812 4.8
condition 22 560 3.9
subroutine 50 283 17.6
pod 106 161 65.8
total 457 4346 10.5


line stmt bran cond sub pod time code
1             package WWW::Mechanize::Chrome;
2 68     68   8293978 use strict;
  68         720  
  68         1991  
3 68     68   415 use warnings;
  68         142  
  68         1897  
4 68     68   30114 use Filter::signatures;
  68         1611064  
  68         460  
5 68     68   2413 no warnings 'experimental::signatures';
  68         152  
  68         2236  
6 68     68   387 use feature 'signatures';
  68         171  
  68         2120  
7 68     68   28899 use PerlX::Maybe;
  68         148724  
  68         312  
8 68     68   2704 use File::Spec;
  68         162  
  68         1688  
9 68     68   31554 use HTTP::Response;
  68         1722144  
  68         2356  
10 68     68   549 use HTTP::Headers;
  68         165  
  68         1796  
11 68     68   402 use Scalar::Util qw( blessed weaken);
  68         156  
  68         4107  
12 68     68   440 use File::Basename;
  68         173  
  68         5103  
13 68     68   436 use Carp qw(croak carp);
  68         157  
  68         3114  
14 68     68   31145 use WWW::Mechanize::Link;
  68         26082  
  68         2004  
15 68     68   32452 use IO::Socket::INET;
  68         946983  
  68         423  
16 68     68   64572 use Chrome::DevToolsProtocol;
  68         245  
  68         2456  
17 68     68   35861 use Chrome::DevToolsProtocol::Target;
  68         206  
  68         3280  
18 68     68   35709 use WWW::Mechanize::Chrome::Node;
  68         202  
  68         2171  
19 68     68   482 use JSON;
  68         146  
  68         296  
20 68     68   39827 use MIME::Base64 'decode_base64';
  68         47340  
  68         4176  
21 68     68   474 use Data::Dumper;
  68         143  
  68         2996  
22 68     68   43117 use Storable 'dclone';
  68         214209  
  68         4363  
23 68     68   35126 use HTML::Selector::XPath 'selector_to_xpath';
  68         181892  
  68         4289  
24 68     68   33347 use HTTP::Cookies::ChromeDevTools;
  68         218  
  68         2670  
25 68     68   2110 use POSIX ':sys_wait_h';
  68         20219  
  68         624  
26             #use Future::IO;
27 68     68   155089 use Future::Utils 'repeat';
  68         153975  
  68         4021  
28 68     68   565 use Time::HiRes ();
  68         155  
  68         1252  
29 68     68   36138 use Encode 'encode';
  68         665043  
  68         321942  
30              
31             our $VERSION = '0.70';
32             our @CARP_NOT;
33              
34             # add Browser.setPermission , .grantPermission for
35             # restricting/allowing recording, clipboard, idleDetection, ...
36              
37             =encoding utf-8
38              
39             =head1 NAME
40              
41             WWW::Mechanize::Chrome - automate the Chrome browser
42              
43             =head1 SYNOPSIS
44              
45             use Log::Log4perl qw(:easy);
46             use WWW::Mechanize::Chrome;
47              
48             Log::Log4perl->easy_init($ERROR); # Set priority of root logger to ERROR
49             my $mech = WWW::Mechanize::Chrome->new();
50             $mech->get('https://google.com');
51              
52             $mech->eval_in_page('alert("Hello Chrome")');
53             my $png = $mech->content_as_png();
54              
55             A collection of other L<Examples|WWW::Mechanize::Chrome::Examples> is available
56             to help you get started.
57              
58             =head1 DESCRIPTION
59              
60             Like L<WWW::Mechanize>, this module automates web browsing with a Perl object.
61             Fetching and rendering of web pages is delegated to the Chrome (or Chromium)
62             browser by starting an instance of the browser and controlling it with L<Chrome
63             DevTools|https://developers.google.com/web/tools/chrome-devtools/>.
64              
65             =head2 Advantages Over L<WWW::Mechanize>
66              
67             The Chrome browser provides advanced abilities useful for automating modern
68             web applications that are not (yet) possible with L<WWW::Mechanize> alone:
69              
70             =over 4
71              
72             =item *
73              
74             Page content can be created or modified with JavaScript. You can also execute
75             custom JavaScript code on the page content.
76              
77             =item *
78              
79             Page content can be selected with CSS selectors.
80              
81             =item *
82              
83             Screenshots of the rendered page as an image or PDF file.
84              
85             =back
86              
87             =head2 Disadvantages
88              
89             Installation of a Chrome compatible browser is required. There are some quirks
90             including sporadic, but harmless, error messages issued by the browser when
91             run with with DevTools.
92              
93             =head2 A Brief Operational Overview
94              
95             C<WWW::Mechanize::Chrome> (WMC) leverages developer tools built into Chrome and
96             Chrome-like browsers to control a browser instance programatically. You can use
97             WMC to automate tedious tasks, test web applications, and perform web scraping
98             operations.
99              
100             Typically, WMC is used to launch both a I<host> instance of the browser and
101             provide a I<client> instance of the browser. The host instance of the browser is
102             visible to you on your desktop (unless the browser is running in "headless"
103             mode, in which case it will not open in a window). The client instance is the
104             Perl program you write with the WMC module to issue commands to control the host
105             instance. As you navigate and "click" on various nodes in the client browser,
106             you watch the host browser respond to these actions as if by magic.
107              
108             This magic happens as a result of commands that are issued from your client to
109             the host using Chrome's DevTools Protocol which implements the http protocol to
110             send JSON data structures. The host also responds to the client with JSON to
111             describe the web pages it has loaded. WMC conveniently hides the complexity of
112             the lower level communications between the client and host browsers and wraps
113             them in a Perl object to provide the easy-to-use methods documented here.
114              
115             =head1 OPTIONS
116              
117             =head2 C<< WWW::Mechanize::Chrome->new( %options ) >>
118              
119             my $mech = WWW::Mechanize::Chrome->new(
120             headless => 0,
121             );
122              
123             =over 4
124              
125             =item B<autodie>
126              
127             autodie => 0 # make HTTP errors non-fatal
128              
129             By default, C<autodie> is set to true. If an HTTP error is encountered, the
130             program dies along with its associated browser instances. This frees you from
131             having to write error checks after every request. Setting this value to false
132             makes HTTP errors non-fatal, allowing the program to continue running if
133             there is an error.
134              
135             =item B<headless>
136              
137             Don't display a browser window. Default is to display a browser
138             window.
139              
140             =item B<host>
141              
142             =item B<listen_host>
143              
144             Set the host the browser listens on:
145              
146             host => '192.168.1.2'
147             host => 'localhost'
148              
149             Defaults to C<127.0.0.1>. The browser will listen for commands on the
150             specified host. The host address should be inaccessible from the internet.
151              
152             =item B<port>
153              
154             port => 9223 # set port the launched browser will use for remote operation
155              
156             Defaults to C<9222>. Commands to the browser will be issued through this port.
157              
158             =item B<tab>
159              
160             Specify the browser tab the Chrome browser will use:
161              
162             tab => 'current'
163             tab => qr/PerlMonks/
164              
165             By default, a web page is opened in a new browser tab. Setting C<tab> to
166             C<current> will use the current, active tab instead. Alternatively, to use an
167             existing inactive tab, you can pass a regular expression to match against the
168             existing tab's title. A false value implements the default behavior and a new
169             tab will be created.
170              
171             =item B<autoclose>
172              
173             autoclose => 0 # keep tab open after program end
174              
175             By default, C<autoclose> is set to true, closing the tab opened when running
176             your code. If C<autoclose> is set to a false value, the tab will remain open
177             even after the program has finished.
178              
179             =item B<launch_exe>
180              
181             Set the name and/or path to the browser's executable program:
182              
183             launch_exe => 'name-of-chrome-executable' # for non-standard executable names
184             launch_exe => '/path/to/executable' # for non-standard paths
185             launch_exe => '/path/to/executable/chrome' # full path
186              
187             By default, C<WWW::Mechanize::Chrome> will search the appropriate paths for
188             Chrome's executable file based on the operating system. Use this option to set
189             the path to your executable if it is in a non-standard location or if the
190             executable has a non-standard name.
191              
192             The default paths searched are those found in C<$ENV{PATH}>. For OS X, the user
193             and system C<Application> directories are also searched. The default values for
194             the executable file's name are C<chrome> on Windows, C<Google Chrome> on OS X,
195             and C<google-chrome> elsewhere.
196              
197             If you want to use Chromium, you must specify that explicitly with something
198             like:
199              
200             launch_exe => 'chromium-browser', # if Chromium is named chromium-browser on your OS
201              
202             Results my vary for your operating system. Use the full path to the browser's
203             executable if you are having issues. You can also set the name of the executable
204             file with the C<$ENV{CHROME_BIN}> environment variable.
205              
206             =item B<cleanup_signal>
207              
208             cleanup_signal => 'SIGKILL'
209              
210             The signal that is sent to Chrome to shut it down. On Linuxish OSes, this
211             will be C<TERM>, on OSX and Windows it will be C<KILL>.
212              
213             =item B<start_url>
214              
215             start_url => 'http://perlmonks.org' # Immediately navigate to a given URL
216              
217             By default, the browser will open with a blank tab. Use the C<start_url> option
218             to open the browser to the specified URL. More typically, the C<< ->get >>
219             method is use to navigate to URLs.
220              
221             =item B<launch_arg>
222              
223             Pass additional switches and parameters to the browser's executable:
224              
225             launch_arg => [ "--some-new-parameter=foo", "--another-option" ]
226              
227             Examples of other useful parameters include:
228              
229             '--start-maximized',
230             '--window-size=1280x1696'
231             '--ignore-certificate-errors'
232              
233             '--disable-web-security',
234             '--allow-running-insecure-content',
235              
236             '--load-extension'
237             '--no-sandbox'
238             '--password-store=basic'
239              
240             =item B<separate_session>
241              
242             separate_session => 1 # create a new, empty session
243              
244             This creates an empty, fresh Chrome session without any cookies. Setting this
245             will disregard any B<data_directory> setting.
246              
247             =item B<incognito>
248              
249             incognito => 1 # open the browser in incognito mode
250              
251             Defaults to false. Set to true to launch the browser in incognito mode.
252              
253             Most likely, you want to use B<separate_session> instead.
254              
255             =item B<data_directory>
256              
257             data_directory => '/path/to/data/directory' # set the data directory
258              
259             By default, an empty data directory is used. Use this setting to change the
260             base data directory for the browsing session.
261              
262             use File::Temp 'tempdir';
263             # create a fresh Chrome every time
264             my $mech = WWW::Mechanize::Chrome->new(
265             data_directory => tempdir(CLEANUP => 1 ),
266             );
267              
268             Using the "main" Chrome cookies:
269              
270             my $mech = WWW::Mechanize::Chrome->new(
271             data_directory => '/home/corion/.config/chromium',
272             );
273              
274             =item B<profile>
275              
276             profile => 'ProfileDirectory' # set the profile directory
277              
278             By default, your current user profile directory is used. Use this setting
279             to change the profile directory for the browsing session.
280              
281             You will need to set the B<data_directory> as well, so that Chrome finds the
282             profile within the data directory. The profile directory/name itself needs
283             to be a single directory name, not the full path. That single directory name
284             will be relative to the data directory.
285              
286             =item B<wait_file>
287              
288             wait_file => "$tempdir/CrashpadMetrics-active.pma"
289              
290             When shutting down, wait until this file does not exist anymore or can be
291             deleted. This can help making sure that the Chrome process has really shut
292             down.
293              
294             =item B<startup_timeout>
295              
296             startup_timeout => 5 # set the startup timeout value
297              
298             Defaults to 20, the maximum number of seconds to wait for the browser to launch.
299             Higher or lower values can be set based on the speed of the machine. The
300             process attempts to connect to the browser once each second over the duration
301             of this setting.
302              
303             =item B<driver>
304              
305             driver => $driver_object # specify the driver object
306              
307             Use a L<Chrome::DevToolsProtocol::Target> object that has been manually constructed.
308              
309             =item B<report_js_errors>
310              
311             report_js_errors => 1 # turn javascript error reporting on
312              
313             Defaults to false. If true, tests for Javascript errors and warns after each
314             request are run. This is useful for testing with C<use warnings qw(fatal)>.
315              
316             =item B<mute_audio>
317              
318             mute_audio => 0 # turn sounds on
319              
320             Defaults to true (sound off). A false value turns the sound on.
321              
322             =item B<background_networking>
323              
324             background_networking => 1 # turn background networking on
325              
326             Defaults to false (off). A true value enables background networking.
327              
328             =item B<client_side_phishing_detection>
329              
330             client_side_phishing_detection => 1 # turn client side phishing detection on
331              
332             Defaults to false (off). A true value enables client side phishing detection.
333              
334             =item B<component_update>
335              
336             component_update => 1 # turn component updates on
337              
338             Defaults to false (off). A true value enables component updates.
339              
340             =item B<default_apps>
341              
342             default_apps => 1 # turn default apps on
343              
344             Defaults to false (off). A true value enables default apps.
345              
346             =item B<hang_monitor>
347              
348             hang_monitor => 1 # turn the hang monitor on
349              
350             Defaults to false (off). A true value enables the hang monitor.
351              
352             =item B<hide_scrollbars>
353              
354             hide_scrollbars => 1 # hide the scrollbars
355              
356             Defaults to false (off). A true value will hide the scrollbars.
357              
358             =item B<infobars>
359              
360             infobars => 1 # turn infobars on
361              
362             Defaults to false (off). A true value will turn infobars on.
363              
364             =item B<popup_blocking>
365              
366             popup_blocking => 1 # block popups
367              
368             Defaults to false (off). A true value will block popups.
369              
370             =item B<prompt_on_repost>
371              
372             prompt_on_repost => 1 # allow prompts when reposting
373              
374             Defaults to false (off). A true value will allow prompts when reposting.
375              
376             =item B<save_password_bubble>
377              
378             save_password_bubble => 1 # allow the display of the save password bubble
379              
380             Defaults to false (off). A true value allows the save password bubble to be
381             displayed.
382              
383             =item B<sync>
384              
385             sync => 1 # turn syncing on
386              
387             Defaults to false (off). A true value turns syncing on.
388              
389             =item B<web_resources>
390              
391             web_resources => 1 # turn web resources on
392              
393             Defaults to false (off). A true value turns web resources on.
394              
395             =item B<json_log_file>
396              
397             Filename to log all JSON communications to, one line per message/event/reply
398              
399             =item B<json_log_fh>
400              
401             Filehandle to log all JSON communications to, one line per message/event/reply
402              
403             Open this filehandle via
404              
405             open my $fh, '>:utf8', $logfilename
406             or die "Couldn't create '$logfilename': $!";
407              
408             =back
409              
410             The C<< $ENV{WWW_MECHANIZE_CHROME_TRANSPORT} >> variable can be set to a
411             different transport class to override the default L<transport
412             class|Chrome::DevToolsProtcol::Transport>. This is primarily used for testing
413             but can also help eliminate introducing bugs from the underlying websocket
414             implementation(s).
415              
416             The C<< $ENV{WWW_MECHANIZE_CHROME_CONNECTION_STYLE} >> variable can be set to
417             either C<websocket> or C<pipe> to specify the kind of transport that you
418             want to use.
419              
420             The C<pipe> transport is only available on unixish OSes and only with Chrome
421             v72 onwards.
422              
423             =head1 METHODS
424              
425             =cut
426              
427             sub build_command_line {
428 2     2 0 8 my( $class, $options )= @_;
429              
430 2         21 my @program_names = $class->default_executable_names( $options->{launch_exe} );
431              
432 2         12 my( $program, $error) = $class->find_executable(\@program_names);
433 2 50       415 croak $error if ! $program;
434              
435             # Convert the path to an absolute filename, so we can chdir() later
436 0   0     0 $program = File::Spec->rel2abs( $program ) || $program;
437              
438 0   0     0 $options->{ launch_arg } ||= [];
439              
440             # We want to read back the URL we can use to talk to Chrome
441 0 0       0 if( $^O =~ /mswin/i ) {
442             #push @{ $options->{launch_arg}}, '--v=0', '--enable-logging'; # v79 bad, v78 bad, v77 bad, v76 bad, v75 bad, v70 bad
443 0         0 push @{ $options->{launch_arg}}, '--v=0'; # v79 OK, v62 OK, v61 bad
  0         0  
444             };
445              
446 0 0       0 if( $options->{pipe}) {
447 0         0 push @{ $options->{ launch_arg }}, "--remote-debugging-pipe";
  0         0  
448             } else {
449              
450             $options->{port} //= 9222
451 0 0 0     0 if ! exists $options->{port};
452              
453 0 0       0 if (exists $options->{port}) {
454 0   0     0 $options->{port} ||= 0;
455 0         0 push @{ $options->{ launch_arg }}, "--remote-debugging-port=$options->{ port }";
  0         0  
456 0         0 push @{ $options->{ launch_arg }}, "--remote-allow-origins=*";
  0         0  
457             };
458              
459 0 0 0     0 if ($options->{listen_host} || $options->{host} ) {
460 0   0     0 my $host = $options->{listen_host} || $options->{host};
461 0         0 push @{ $options->{ launch_arg }}, "--remote-debugging-address=$host";
  0         0  
462             };
463             };
464              
465 0 0       0 if ($options->{incognito}) {
466 0         0 push @{ $options->{ launch_arg }}, "--incognito";
  0         0  
467             };
468              
469 0 0       0 if ($options->{data_directory}) {
470 0         0 push @{ $options->{ launch_arg }}, "--user-data-dir=$options->{ data_directory }";
  0         0  
471             };
472              
473 0 0       0 if (my $profile = $options->{profile}) {
474 0 0       0 if(! $options->{data_directory}) {
    0          
475 0         0 croak "Cannot use the 'profile' option without also having 'data_directory'";
476             } elsif( $profile =~ m![/\\]! ) {
477 0         0 my $rel = File::Spec->rel2abs($profile, $options->{data_directory});
478 0 0       0 if( $rel =~ m![/\\]!) {
479 0         0 croak "The 'profile' option may not contain the path separator";
480             } else {
481 0         0 $profile = $rel;
482             };
483             }
484              
485 0         0 push @{ $options->{ launch_arg }}, "--profile-directory=$profile";
  0         0  
486             };
487              
488 0 0 0     0 if( ! exists $options->{enable_automation} || $options->{enable_automation}) {
489 0         0 push @{ $options->{ launch_arg }}, "--enable-automation";
  0         0  
490             };
491              
492 0 0 0     0 if( ! exists $options->{enable_first_run} || ! $options->{enable_first_run}) {
493 0         0 push @{ $options->{ launch_arg }}, "--no-first-run";
  0         0  
494             };
495              
496 0 0 0     0 if( ! exists $options->{mute_audio} || $options->{mute_audio}) {
497 0         0 push @{ $options->{ launch_arg }}, "--mute-audio";
  0         0  
498             };
499              
500 0   0     0 my $no_sandbox = $options->{no_sandbox} || ! (exists $options->{no_zygote});
501 0 0       0 if( ! $no_sandbox) {
502 0         0 push @{ $options->{ launch_arg }}, "--no-zygote";
  0         0  
503             };
504              
505 0 0       0 if( $no_sandbox) {
506 0         0 push @{ $options->{ launch_arg }}, "--no-sandbox";
  0         0  
507             };
508              
509 0 0       0 if( $options->{hide_scrollbars}) {
510 0         0 push @{ $options->{ launch_arg }}, "--hide-scrollbars";
  0         0  
511             };
512              
513             # Yes, that name is horrible
514 0 0       0 if( $options->{safebrowsing_auto_update}) {
515             } else {
516 0         0 push @{ $options->{ launch_arg }}, "--safebrowsing-disable-auto-update";
  0         0  
517             };
518              
519 0 0 0     0 if( ! exists $options->{default_browser_check} || ! $options->{default_browser_check}) {
520 0         0 push @{ $options->{ launch_arg }}, "--no-default-browser-check";
  0         0  
521             };
522              
523 0 0       0 if( exists $options->{disable_prompt_on_repost}) {
524 0         0 carp "Option 'disable_prompt_on_repost' is deprecated, use prompt_on_repost instead";
525 0         0 $options->{prompt_on_repost} = !$options->{disable_prompt_on_repost};
526             };
527              
528 0         0 for my $option (qw(
529             background_networking
530             breakpad
531             client_side_phishing_detection
532             component_update
533             hang_monitor
534             prompt_on_repost
535             sync
536             web_resources
537             default_apps
538             popup_blocking
539             gpu
540             domain_reliability
541             )) {
542 0         0 (my $optname = $option) =~ s!_!-!g;
543 0 0       0 if( ! exists $options->{$option}) {
    0          
544 0         0 push @{ $options->{ launch_arg }}, "--disable-$optname";
  0         0  
545             } elsif( ! (my $value = delete $options->{$option})) {
546 0         0 push @{ $options->{ launch_arg }}, "--disable-$optname";
  0         0  
547             };
548             };
549              
550 0         0 push @{ $options->{ launch_arg }}, "--headless"
551 0 0       0 if $options->{ headless };
552              
553 0         0 push @{ $options->{ launch_arg }}, "$options->{start_url}"
554 0 0       0 if exists $options->{start_url};
555              
556 0 0 0     0 my $quoted_program = ($^O =~ /mswin/i and $program =~ /[\s|<>&]/)
557             ? qq("$program")
558             : $program;
559              
560 0         0 my @cmd=( $program, @{ $options->{launch_arg}} );
  0         0  
561              
562             @cmd
563 0         0 };
564              
565             =head2 C<< WWW::Mechanize::Chrome->find_executable >>
566              
567             my $chrome = WWW::Mechanize::Chrome->find_executable();
568              
569             my $chrome = WWW::Mechanize::Chrome->find_executable(
570             'chromium.exe',
571             '.\\my-chrome-66\\',
572             );
573              
574             my( $chrome, $diagnosis ) = WWW::Mechanize::Chrome->find_executable(
575             ['chromium-browser','google-chrome'],
576             './my-chrome-66/',
577             );
578             die $diagnosis if ! $chrome;
579              
580             Finds the first Chrome executable in the path (C<$ENV{PATH}>). For Windows, it
581             also looks in C<< $ENV{ProgramFiles} >>, C<< $ENV{ProgramFiles(x86)} >>
582             and C<< $ENV{"ProgramFilesW6432"} >>. For OSX it also looks in the user home
583             directory as given through C<< $ENV{HOME} >>.
584              
585             This is used to find the default Chrome executable if none was given through
586             the C<launch_exe> option or if the executable is given and does not exist
587             and does not contain a directory separator.
588              
589             =cut
590              
591 131     131 0 1559 sub default_executable_names( $class, @other ) {
  131         257  
  131         259  
  131         265  
592             my @program_names
593 134         498 = grep { defined($_) } (
594             $ENV{CHROME_BIN},
595 131         486 @other,
596             );
597 131 100       523 if( ! @program_names ) {
598 129 50       1037 push @program_names,
    50          
599             $^O =~ /mswin/i ? 'chrome.exe'
600             : $^O =~ /darwin/i ? ('Google Chrome', 'Chromium')
601             : ('google-chrome', 'chromium-browser', 'chromium')
602             };
603             @program_names
604 131         498 }
605              
606             # Returns additional directories where the default executable can be found
607             # on this OS
608 130     130 0 270 sub additional_executable_search_directories( $class, $os_style=$^O ) {
  130         222  
  130         435  
  130         220  
609 130         264 my @search;
610 130 50       627 if( $os_style =~ /MSWin/i ) {
    50          
611             push @search,
612 0         0 map { "$_\\Google\\Chrome\\Application\\" }
613 0         0 grep {defined}
614             ($ENV{'ProgramFiles'},
615             $ENV{'ProgramFiles(x86)'},
616             $ENV{"ProgramFilesW6432"},
617 0         0 $ENV{"LOCALAPPDATA"},
618             );
619             } elsif( $os_style =~ /darwin/i ) {
620 0         0 for my $path ('/Applications/Google Chrome.app/Contents/MacOS',
621             '/Applications/Chromium.app/Contents/MacOS') {
622             push @search,
623 0         0 grep { -d $_ }
624             $path,
625 0         0 $ENV{"HOME"} . "/$path";
626             };
627             }
628             @search
629 130         363 }
630              
631 131     131 1 533979 sub find_executable( $class, $program=[$class->default_executable_names], @search) {
  131         345  
  131         483  
  131         268  
  131         227  
632 131         301 my $looked_for = '';
633 131 100       472 if( ! ref $program) {
634 1         3 $program = [$program]
635             };
636 131         707 my $program_name = join ", ", map { qq('$_') } @$program;
  389         1216  
637              
638 131 50       377 if( my($first_program) = grep { -x $_ } @$program) {
  389         3726  
639             # We've got a complete path, done!
640 0         0 return $first_program
641             };
642              
643             # Not immediately found, so we need to search
644 131         437 my @without_path = grep { !m![/\\]! } @$program;
  389         1218  
645              
646 131 100       437 if( @without_path) {
647 130         2485 push @search, File::Spec->path();
648 130         566 push @search, $class->additional_executable_search_directories();
649 130         552 $looked_for = ' in searchpath ' . join " ", @search;
650             };
651              
652 131         302 my $found;
653              
654 131         321 for my $path (@search) {
655 1152         2570 for my $p (@without_path) {
656 3438         22603 my $this = File::Spec->catfile( $path, $p );
657 3438 50       31293 if( -x $this ) {
658 0         0 $found = $this;
659 0         0 last;
660             };
661             };
662             };
663              
664 131 50       579 if( wantarray ) {
665 131         295 my $msg;
666 131 50       507 if( ! $found) {
667 131         550 $msg = "No executable like $program_name found$looked_for";
668             };
669 131         922 return $found, $msg
670             } else {
671 0         0 return $found
672             };
673             }
674              
675 0     0   0 sub _find_free_port( $class, $start ) {
  0         0  
  0         0  
  0         0  
676 0         0 my $port = $start;
677 0         0 while (1) {
678 0 0       0 $port++, next unless IO::Socket::INET->new(
679             Listen => 5,
680             Proto => 'tcp',
681             Reuse => 1,
682             LocalPort => $port
683             );
684 0         0 last;
685             }
686 0         0 $port;
687             }
688              
689 0     0   0 sub _wait_for_socket_connection( $class, $host, $port, $timeout=20 ) {
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
690 0         0 my $res = 0;
691 0         0 my $wait = time + $timeout;
692 0         0 while ( time < $wait ) {
693 0         0 my $t = time;
694 0         0 my $socket = IO::Socket::INET->new(
695             PeerHost => $host,
696             PeerPort => $port,
697             Proto => 'tcp',
698             );
699 0 0       0 if( $socket ) {
700 0         0 close $socket;
701             #Time::HiRes::sleep(0.5);
702 0         0 $res = 1;
703 0         0 last;
704             };
705 0 0       0 Time::HiRes::sleep(0.1) if time - $t < 1;
706             }
707              
708 0         0 return $res
709             };
710              
711 0     0 0 0 sub spawn_child_win32( $self, $method, @cmd ) {
  0         0  
  0         0  
  0         0  
  0         0  
712 0 0       0 croak "Only websocket communication is supported on $^O, not '$method'"
713             if $method ne 'websocket';
714              
715             # Our store for the filehandles
716 0         0 my (%child, %parent);
717              
718 0         0 require IPC::Open3;
719 0         0 require Symbol;
720 0         0 $parent{child_output} = Symbol::gensym();
721             my $pid = IPC::Open3::open3(
722             undef, $parent{ child_output }, $parent{ child_output },
723             @cmd
724 0         0 );
725              
726 0         0 return $pid, $parent{write}, $parent{read}, $parent{child_output};
727             }
728              
729 0     0 0 0 sub spawn_child_posix( $self, $method, @cmd ) {
  0         0  
  0         0  
  0         0  
  0         0  
730 0         0 require POSIX;
731 0         0 POSIX->import("setsid");
732              
733             # Our store for the filehandles
734 0         0 my (%child, %parent);
735              
736 0 0       0 if( $method eq 'pipe' ) {
737             # Now, we want to have file handles with fileno=3 and fileno=4
738             # to talk to Chrome v72+
739              
740             # Just open some filehandles to push the filenos above 4 for sure:
741 0         0 open my $dummy_fh, '>', '/dev/null';
742 0         0 open my $dummy_fh2, '>', '/dev/null';
743              
744 0         0 pipe $child{read}, $parent{write};
745 0         0 pipe $parent{read}, $child{write};
746              
747 0         0 close $dummy_fh;
748 0         0 close $dummy_fh2;
749             } else {
750             # We want to read back the websocket URL from the STDOUT (well STDERR)
751             # of the child
752 0         0 pipe $parent{child_output}, $child{stdout};
753 0         0 $parent{child_output}->autoflush(1);
754             };
755              
756             # daemonize
757 0 0       0 defined(my $pid = fork()) || die "can't fork: $!";
758 0 0       0 if( $pid ) { # non-zero now means I am the parent
759              
760             # Close all child filehandles
761 0         0 for my $v (values(%child)) {
762 0         0 close $v;
763             };
764 0         0 return $pid, $parent{write}, $parent{read}, $parent{child_output};
765             };
766              
767             # We are the child, close about everything, then exec
768 0 0       0 chdir("/") || die "can't chdir to /: $!";
769 0 0       0 (setsid() != -1) || die "Can't start a new session: $!";
770 0 0       0 open(STDIN, "< /dev/null") || die "can't read /dev/null: $!";
771 0 0       0 if( 'pipe' eq $method ) {
772 0 0       0 open(STDERR, ">&", STDOUT) || die "can't dup stdout: $!";
773 0 0       0 open(STDOUT, "> /dev/null") || die "can't talk to new STDOUT: $!";
774             } else {
775 0 0       0 open(STDERR, ">&", $child{stdout}) || die "can't dup stdout: $!";
776 0 0       0 open(STDOUT, ">&", $child{stdout}) || die "can't talk to new STDOUT: $!";
777             };
778              
779 0         0 my ($from_chrome, $to_chrome);
780 0 0       0 if( $method eq 'pipe' ) {
781             # We want handles 0,1,2,3,4 to be inherited by Chrome
782 0         0 $^F = 4;
783              
784             # Set up FD 3 and 4 for Chrome to read/write
785 0 0       0 open($from_chrome, '<&', $child{read})|| die "can't open reader pipe: $!";
786 0 0       0 open($to_chrome, '>&', $child{write}) || die "can't open writer pipe: $!";
787             }
788 0         0 for my $v (values(%parent)) {
789 0         0 close $v;
790             };
791             #close $parent{child_output};
792 0         0 exec @cmd;
793 0         0 warn "Child couldn't launch [@cmd]: $!";
794 0         0 exit 1;
795             }
796              
797 0     0 0 0 sub spawn_child( $self, $method, @cmd ) {
  0         0  
  0         0  
  0         0  
  0         0  
798 0         0 my ($pid, $to_chrome, $from_chrome, $chrome_stdout);
799 0 0       0 if( $^O =~ /mswin/i ) {
800 0         0 ($pid,$to_chrome,$from_chrome, $chrome_stdout) = $self->spawn_child_win32($method, @cmd)
801             } else {
802 0         0 ($pid,$to_chrome,$from_chrome, $chrome_stdout) = $self->spawn_child_posix($method, @cmd)
803             };
804 0         0 $self->log('debug', "Spawned child as $pid, communicating via $method");
805              
806 0         0 return ($pid,$to_chrome,$from_chrome, $chrome_stdout)
807             }
808              
809 0     0 0 0 sub read_devtools_url( $self, $fh, $lines = 10 ) {
  0         0  
  0         0  
  0         0  
  0         0  
810             # We expect the output within the first 10 lines...
811 0         0 my $devtools_url;
812              
813 0   0     0 while( $lines-- and ! defined $devtools_url and ! eof($fh)) {
      0        
814 0         0 my $line = <$fh>;
815 0 0       0 last unless defined $line;
816 0         0 $line =~ s!\s+$!!;
817 0         0 $self->log('trace', "[[$line]]");
818 0 0       0 if( $line =~ m!^DevTools listening on (ws:\S+)$!) {
    0          
819 0         0 $devtools_url = $1;
820 0         0 $self->log('trace', "Found ws endpoint from child output as '$devtools_url'");
821 0         0 last;
822             } elsif( $line =~ m!ERROR:headless_shell.cc! ) {
823 0         0 die "Chrome launch error: $line";
824             }
825             };
826 0         0 $devtools_url
827             };
828              
829 1     1   2 sub _build_log( $self ) {
  1         3  
  1         6  
830 1         8 require Log::Log4perl;
831 1         11 Log::Log4perl->get_logger(__PACKAGE__);
832             }
833              
834             # The generation of node ids
835 0     0   0 sub _generation( $self, $val=undef ) {
  0         0  
  0         0  
  0         0  
836 0 0       0 @_ == 2 and $self->{_generation} = $_[1];
837             $self->{_generation}
838 0         0 };
839              
840 0     0 0 0 sub new_generation( $self ) {
  0         0  
  0         0  
841 0   0     0 $self->_generation( ($self->_generation() ||0) +1 );
842             }
843              
844 0     0 0 0 sub log( $self, $level, $message, @args ) {
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
845 0         0 my $logger = $self->{log};
846 0 0       0 if( !@args ) {
847 0         0 $logger->$level( $message )
848             } else {
849 0         0 my $enabled = "is_$level";
850 0 0       0 $logger->$level( join " ", $message, Dumper @args )
851             if( $logger->$enabled );
852             };
853             }
854              
855 4     4   3901 sub _preferred_transport($class, $options) {
  4         11  
  4         55  
  4         10  
856             ref( $options->{ transport } )
857             || $options->{ transport }
858             || $ENV{ WWW_MECHANIZE_CHROME_TRANSPORT }
859 4 50 66     49 || 'Chrome::DevToolsProtocol::Transport'
      66        
860             }
861              
862             # Find out what connection style (websocket, pipe) the user wants:
863 1     1 0 2 sub connection_style( $class, $options ) {
  1         2  
  1         2  
  1         1  
864 1 50       3 if( $options->{pipe} ) {
865 0         0 return 'pipe'
866             } else {
867 1         3 my $t = $class->_preferred_transport($options);
868 1 50       75 eval "require $t; 1"
869             or warn $@;
870 1   50     9 return $t->new->type || 'websocket';
871             };
872             };
873              
874 1     1 0 3 sub new_future($class, %options) {
  1         2  
  1         2  
  1         2  
875              
876 1 50       5 if (! exists $options{ autodie }) {
877 1         3 $options{ autodie } = 1
878             };
879              
880 1 50       5 if (! exists $options{ autoclose }) {
881 1         4 $options{ autoclose } = 1
882             };
883              
884 1 50       3 if( ! exists $options{ frames }) {
885 1         3 $options{ frames }= 1;
886             };
887              
888 1 50       3 if( ! exists $options{ download_directory }) {
889 1         3 $options{ download_directory }= '';
890             };
891              
892 1   50     8 $options{ startup_timeout } //= 20;
893              
894 1   50     6 $options{ js_events } ||= [];
895 1 50       64 if( ! exists $options{ transport }) {
896 1         6 $options{ transport } = $class->_preferred_transport(\%options);
897             };
898              
899             $options{start_url} = 'about:blank'
900 1 50       5 unless exists $options{start_url};
901              
902 1   50     5 my $host = $options{ host } || '127.0.0.1';
903 1         3 $options{ host } = $host;
904              
905 1   50     7 $options{ extra_headers } ||= {};
906              
907 1 50       3 if( $options{ separate_session }) {
908 0   0     0 $options{ tab } ||= undef;
909             } else {
910 1   50     5 $options{ tab } ||= 0;
911             }
912 1   33     6 $options{ existing_tab } ||= defined $options{ tab };
913              
914 1 50 33     4 if( $options{ tab } and $options{ tab } eq 'current' ) {
915 0         0 $options{ tab } = 0; # use tab at index 0
916             };
917              
918             # Find out what connection style we need/the user wants
919             my $connection_style = $options{ connection_style }
920             || $ENV{ WWW_MECHANIZE_CHROME_CONNECTION_STYLE }
921 1   33     11 || $class->connection_style( \%options );
922 1 50 33     1329 if( ! $options{ port } and ! $options{ pid } ) {
923 1 50       5 if( $options{ pipe } ) {
924             #if( $^O !~ /mswin32/i ) {
925 0         0 $connection_style = 'pipe';
926             };
927             };
928 1         3 $options{ connection_style } = $connection_style;
929              
930 1 50       3 if( ! exists $options{ pipe }) {
931 1         5 $options{ pipe } = 'pipe' eq $connection_style;
932             };
933              
934 1 50 33     14 $options{ cleanup_signal } ||= $^O =~ /mswin32/i ? 'SIGKILL'
    50          
935             : $^O =~ /darwin/i ? 'SIGKILL'
936             : 'SIGTERM';
937              
938 1   33     10 my $self= bless \%options => (ref $class || $class);
939              
940 1   33     12 $self->{log} ||= $self->_build_log;
941              
942 1 50 33     521 if( $options{ pid } ) {
    50          
    50          
943             # Assume some defaults for the already running Chrome executable
944 0   0     0 $options{ port } //= 9222;
945              
946             } elsif ( $options{ driver } and $options{ driver_transport }) {
947             # We already have a connection to some Chrome running
948              
949             } elsif( $options{ port }) {
950             # User has specified a port, so we will tell Chrome to use it
951             # Check whether the port is readily available
952             my $ok = $self->_wait_for_socket_connection(
953             $host,
954             $self->{port},
955 0         0 2 # we don't need a long timeout here since Chrome either runs already
956             # or we need to start it ourselves. But we seem to need two
957             # seconds in most cases on my (fast) machine ...
958             );
959             # If not, launch Chrome with that debugging port
960 0 0       0 if( ! $ok) {
961 0         0 $self->log('debug', "No response on $host:$self->{ port }, launching fresh instance");
962 0         0 $self->_spawn_new_chrome_instance( \%options );
963             };
964              
965             } else {
966             # We want Chrome to tell us the address to use
967 1         4 $options{ port } = 0;
968              
969 1         5 $self->_spawn_new_chrome_instance( \%options );
970             };
971              
972 0         0 my @connection;
973 0 0       0 if( 'pipe' eq $connection_style ) {
    0          
974             @connection = (
975             writer_fh => $options{ writer_fh },
976             reader_fh => $options{ reader_fh },
977 0         0 );
978             } elsif( $options{ endpoint }) {
979             @connection = (
980             endpoint => $options{ endpoint },
981 0         0 );
982             } else {
983             @connection = (
984             port => $options{ port },
985 0         0 host => $host,
986             );
987             };
988              
989 0 0       0 if( my $fn = delete $options{ json_log_file }) {
990 0 0       0 open $options{ json_log_fh }, '>:utf8', $fn
991             or die "Couldn't create '$fn': $!";
992             };
993              
994             # Connect to it via TCP or local pipe
995             $options{ driver_transport } ||= Chrome::DevToolsProtocol->new(
996             @connection,
997             transport => $options{ transport },
998             log => $options{ log },
999             maybe json_log_fh => delete $options{ json_log_fh },
1000 0   0     0 );
1001              
1002             $options{ target } ||= Chrome::DevToolsProtocol::Target->new(
1003             auto_close => 0,
1004             transport => delete $options{ driver_transport },
1005             error_handler => sub {
1006             #warn ref$_[0];
1007             #warn "<<@CARP_NOT>>";
1008             #warn ((caller($_))[0,1,2])
1009             # for 1..4;
1010 0     0   0 local @CARP_NOT = (@CARP_NOT, ref $_[0],'Try::Tiny');
1011             # Reraise the error
1012 0         0 croak $_[1]
1013             },
1014             #transport => $options{ transport },
1015             #log => $options{ log },
1016 0   0     0 );
1017              
1018 0         0 my $reuse_transport = delete $options{ reuse_transport };
1019             my $res = $self->_connect(
1020             reuse => $reuse_transport,
1021             %options,
1022             )->then(sub {
1023 0     0   0 return Future->done( $self )
1024 0         0 });
1025              
1026 0         0 return $res
1027             };
1028              
1029 1     1   3 sub _spawn_new_chrome_instance( $self, $options ) {
  1         2  
  1         3  
  1         2  
1030 1         3 my $class = ref $self;
1031 1         6 my @cmd = $class->build_command_line( $options );
1032 0         0 $self->log('debug', "Spawning for $options->{ connection_style }", \@cmd);
1033             (my( $pid , $to_chrome, $from_chrome, $chrome_stdout ))
1034 0         0 = $self->spawn_child( $options->{ connection_style }, @cmd );
1035 0         0 $options->{ writer_fh } = $to_chrome;
1036 0         0 $options->{ reader_fh } = $from_chrome;
1037 0         0 $self->{pid} = $pid;
1038 0         0 $self->{ kill_pid } = 1;
1039 0 0       0 if( $options->{ connection_style } eq 'pipe') {
1040 0         0 $options->{ writer_fh } = $to_chrome;
1041 0         0 $options->{ reader_fh } = $from_chrome;
1042              
1043             } else {
1044 0 0       0 if( $chrome_stdout ) {
1045             # Synchronously wait for the URL we can connect to
1046             # Maybe this should become part of the transport, or a second
1047             # class to asynchronously wait on a filehandle?!
1048 0         0 $options->{ endpoint } = $self->read_devtools_url( $chrome_stdout );
1049 0         0 close $chrome_stdout;
1050              
1051 0 0       0 if( ! $options->{endpoint} ) {
1052 0         0 die join ' ',
1053             "Could not read websocket endpoint from Chrome output.",
1054             "Do you maybe have a non-debug instance of Chrome",
1055             "already running?"
1056             ;
1057             };
1058              
1059             # set up host/port here so it can be used later by other instances
1060 0         0 my $ws = URI->new( $options->{endpoint});
1061 0         0 $options->{port} = $ws->port;
1062 0         0 $options->{host} = $ws->host;
1063              
1064             } else {
1065              
1066             # Try a fresh socket connection, blindly
1067             # Just to give Chrome time to start up, make sure it accepts connections
1068             my $ok = $self->_wait_for_socket_connection(
1069             $options->{ host },
1070             $self->{port},
1071             $self->{startup_timeout}
1072 0         0 );
1073 0 0       0 if( ! $ok) {
1074 0         0 die join ' ',
1075             "Timeout while connecting to $options->{ host }:$self->{port}.",
1076             "Do you maybe have a non-debug instance of Chrome",
1077             "already running?";
1078             };
1079             };
1080             };
1081             }
1082              
1083 1     1 1 681 sub new( $class, %args ) {
  1         2  
  1         4  
  1         2  
1084             # Synchronously connect here, just for easy API compatibility
1085 1         5 return $class->new_future(%args)->get;
1086             }
1087              
1088 0     0   0 sub _setup_driver_future( $self, %options ) {
  0         0  
  0         0  
  0         0  
1089             $self->target->connect(
1090             new_tab => !$options{ existing_tab } || $options{ new_tab },
1091             tab => $options{ tab },
1092             #reuse => $options{ reuse_transport },
1093             separate_session => $options{ separate_session },
1094             start_url => $options{ start_url } ? "".$options{ start_url } : undef,
1095 0     0   0 )->catch( sub(@args) {
  0         0  
  0         0  
1096 0         0 my $err = $args[0];
1097 0 0       0 if( ref $args[1] eq 'HASH') {
1098 68     68   689 use Data::Dumper; warn Dumper $args[1];
  68         189  
  68         247278  
  0         0  
1099 0         0 $err .= $args[1]->{Reason};
1100             };
1101 0         0 Future->fail( $err );
1102             })
1103 0 0 0     0 }
1104              
1105             # This (tries to) connects to the devtools in the browser
1106 0     0   0 sub _connect( $self, %options ) {
  0         0  
  0         0  
  0         0  
1107 0         0 my $err;
1108 0         0 my $setup = $self->_setup_driver_future( %options )
1109 0     0   0 ->catch( sub(@args) {
  0         0  
1110 0         0 $err = $args[0];
1111 0         0 Future->fail( @args );
1112 0         0 });
1113              
1114             # if Chrome started, but so slow or unresponsive that we cannot connect
1115             # to it, kill it manually to avoid waiting for it indefinitely
1116 0 0       0 if ( $err ) {
1117 0 0 0     0 if( $self->{ kill_pid } and my $pid = delete $self->{ pid }) {
1118 0         0 $self->kill_child( 'SIGKILL', $pid, $self->{wait_file} );
1119             };
1120 0         0 croak $err;
1121             }
1122              
1123             # Create new world if needed
1124             # connect to current world/new world
1125              
1126 0         0 my $s = $self;
1127 0         0 weaken $s;
1128              
1129             my $res = $setup->then(sub {
1130 0         0 my $collect_JS_problems = sub( $msg ) {
1131             $s->_handleConsoleAPICall( $msg->{params} )
1132 0     0   0 };
  0         0  
1133             $s->{consoleAPIListener} =
1134 0         0 $s->add_listener( 'Runtime.consoleAPICalled', $collect_JS_problems );
1135             $s->{exceptionThrownListener} =
1136 0         0 $s->add_listener( 'Runtime.exceptionThrown', $collect_JS_problems );
1137             $s->{nodeGenerationChange} =
1138 0         0 $s->add_listener( 'DOM.attributeModified', sub { $s->new_generation() } );
  0         0  
1139 0         0 $s->new_generation;
1140              
1141             my @setup = (
1142             $s->target->send_message('DOM.enable'),
1143             $s->target->send_message('Overlay.enable'),
1144             $s->target->send_message('Page.enable'), # capture DOMLoaded
1145             $s->target->send_message('Network.enable'), # capture network
1146             $s->target->send_message('Runtime.enable'), # capture console messages
1147             #$self->target->send_message('Debugger.enable'), # capture "script compiled" messages
1148             $s->set_download_directory_future($self->{download_directory}),
1149              
1150 0 0       0 keys %{$options{ extra_headers }} ? $s->_set_extra_headers_future( %{$options{ extra_headers }} ) : (),
  0         0  
  0         0  
1151              
1152             # do a dummy search so no nodeId 0 gets used (?!)
1153             # $s->_performSearch(query => '//'),
1154             );
1155              
1156 0 0       0 if( my $agent = delete $options{ user_agent }) {
1157 0         0 push @setup, $s->agent_future( $agent );
1158             };
1159             my $res = Future->wait_all(
1160             @setup,
1161             )->on_done(sub {
1162              
1163             # ->get() doesn't have ->get_future() yet
1164 0 0       0 if( ! (exists $options{ tab } )) {
1165 0         0 $s->get($options{ start_url }); # Reset to clean state, also initialize our frame id
1166             };
1167              
1168             $s->{_fresh_document} = $s->add_listener('DOM.documentUpdated', sub {
1169 0         0 $s->{_currentNodeGeneration}++;
1170 0         0 $s->log('debug', "Need new node ids! Now: $s->{_currentNodeGeneration}");
1171             # Maybe simply ->clear_cached_document is enough?!
1172 0         0 $s->_clear_cached_document;
1173 0         0 });
1174 0         0 });
1175 0         0 });
1176              
1177 0         0 return $res
1178             }
1179              
1180 0     0   0 sub _handleConsoleAPICall( $self, $msg ) {
  0         0  
  0         0  
  0         0  
1181 0 0       0 if( $self->{report_js_errors}) {
1182 0         0 my $desc = $msg->{exceptionDetails}->{exception}->{description};
1183 0         0 my $loc = $msg->{exceptionDetails}->{stackTrace}->{callFrames}->[0]->{url};
1184 0         0 my $line = $msg->{exceptionDetails}->{stackTrace}->{callFrames}->[0]->{lineNumber};
1185 0         0 my $err = "$desc at $loc line $line";
1186 0         0 $self->log('error', $err);
1187             };
1188 0         0 push @{$self->{js_events}}, $msg;
  0         0  
1189             }
1190              
1191 0     0 0 0 sub frameId( $self ) {
  0         0  
  0         0  
1192             $self->{frameId}
1193 0         0 }
1194              
1195 0     0 0 0 sub requestId( $self ) {
  0         0  
  0         0  
1196             $self->{requestId}
1197 0         0 }
1198              
1199             =head2 C<< $mech->chrome_version >>
1200              
1201             print $mech->chrome_version;
1202              
1203             Synonym for C<< ->browser_version >>
1204              
1205             =cut
1206              
1207             =head2 C<< $mech->browser_version >>
1208              
1209             print $mech->browser_version;
1210              
1211             Returns the version of the browser executable being used. This information
1212             needs launching the browser and asking for the version via the network.
1213              
1214             =cut
1215              
1216 1     1 0 3 sub browser_version_from_stdout( $class, $options={} ) {
  1         2  
  1         3  
  1         2  
1217             # We can try to get at the version through the --version command line:
1218             my @cmd = $class->build_command_line({
1219             launch_arg => ['--version'],
1220             headless => 0,
1221             enable_automation => 0,
1222             port => undef,
1223             maybe launch_exe => $options->{launch_exe},
1224 1         13 });
1225 0 0       0 if ($^O =~ /darwin/) {
1226 0         0 s/ /\\ /g for @cmd;
1227             }
1228              
1229 0         0 my $v = readpipe(join " ", @cmd);
1230              
1231             # Chromium 58.0.3029.96 Built on Ubuntu , running on Ubuntu 14.04
1232             # Chromium 76.0.4809.100 built on Debian 10.0, running on Debian 10.0
1233             # Google Chrome 78.0.3904.97
1234             # Mozilla Firefox 87.0
1235 0 0       0 if( $v =~ m!^(.*?)\s+(\d+\.\d+\.\d+\.\d+)\b!) {
    0          
1236 0         0 return "$1/$2"
1237             } elsif($v =~ m!^(Mozilla Firefox)[ /](\d+.\d+)\b!) {
1238 0         0 return "$1/$2.0.0"
1239             } else {
1240 0         0 return; # we didn't find anything
1241             }
1242             }
1243              
1244 0     0 0 0 sub browser_version_from_executable_win32( $class, $options={} ) {
  0         0  
  0         0  
  0         0  
1245 0         0 require Win32::File::VersionInfo;
1246              
1247 0 0       0 my @names = ($options->{launch_exe} ? $options->{launch_exe}: ());
1248 0         0 my ($program,$error) = $class->find_executable( @names );
1249 0 0       0 croak $error if $error;
1250              
1251 0         0 my $info = Win32::File::VersionInfo::GetFileVersionInfo( $program );
1252              
1253             # Find whether we are Chrome* or MS Edge:
1254 0         0 (my $l) = sort (keys %{$info->{Lang}});
  0         0  
1255 0         0 my $name = $info->{Lang}->{ $l }->{"ProductName"};
1256 0 0       0 if( $name eq 'Microsoft Edge' ) {
1257             # Fudge the version to the equivalent Chrome API version
1258 0         0 my $v = $info->{ProductVersion};
1259 0 0       0 if( $v =~ /^11\./ ) {
1260 0         0 $v = "72.0.0.0"; # random guess
1261             } else {
1262 0         0 $v = "78.0.0.0"; # even more random guess
1263             };
1264 0         0 return "Chrome/$v";
1265             } else {
1266 0         0 return "Chrome/$info->{ProductVersion}";
1267             };
1268             }
1269              
1270 1     1 1 4204 sub browser_version( $self, %options ) {
  1         3  
  1         2  
  1         2  
1271 1 50 33     14 if( blessed $self and $self->target ) {
    50          
1272 0         0 return $self->chrome_version_info()->{product};
1273              
1274             } elsif( $^O !~ /mswin/i ) {
1275 1         4 my $version = $self->browser_version_from_stdout(\%options);
1276 0 0       0 if( $version ) {
1277 0         0 return $version;
1278             };
1279              
1280             } else {
1281 0         0 $self->browser_version_from_executable_win32( \%options )
1282             };
1283             }
1284              
1285             *chrome_version =
1286             *chrome_version = \&browser_version;
1287              
1288             =head2 C<< $mech->chrome_version_info >>
1289              
1290             print $mech->chrome_version_info->{product};
1291              
1292             Returns the version information of the Chrome executable and various other
1293             APIs of Chrome that the object is connected to.
1294              
1295             =cut
1296              
1297 0     0 1 0 sub chrome_version_info( $self ) {
  0         0  
  0         0  
1298 0   0     0 $self->{chrome_version} ||= do {
1299             #$self->target->version_info->get;
1300 0         0 $self->target->getVersion->get;
1301             };
1302             }
1303              
1304             =head2 C<< $mech->driver >>
1305              
1306             B<deprecated> - use C<< ->target >> instead
1307              
1308             my $driver = $mech->driver
1309              
1310             Access the L<Chrome::DevToolsProtocol> instance connecting to Chrome.
1311              
1312             Deprecated, don't use this anymore. Most likely you want to use C<< ->target >>
1313             to talk to the Chrome tab or C<< ->transport >> to talk to the Chrome instance.
1314              
1315             =cut
1316              
1317             sub driver {
1318 0     0 1 0 $_[0]->target
1319             };
1320              
1321             =head2 C<< $mech->target >>
1322              
1323             my $target = $mech->target
1324              
1325             Access the L<Chrome::DevToolsProtocol::Target> instance connecting to the
1326             Chrome tab we use.
1327              
1328             =cut
1329              
1330             sub target {
1331             $_[0]->{target}
1332 1     1 1 6 };
1333              
1334             =head2 C<< $mech->transport >>
1335              
1336             my $transport = $mech->transport
1337              
1338             Access the L<Chrome::DevToolsProtocol::Transport> instance connecting to the
1339             Chrome instance.
1340              
1341             =cut
1342              
1343             sub transport {
1344 0     0 1 0 $_[0]->driver->transport
1345             };
1346              
1347             =head2 C<< $mech->tab >>
1348              
1349             my $tab = $mech->tab
1350              
1351             Access the tab hash of the L<Chrome::DevToolsProtocol::Target> instance.
1352             This represents the tab we control.
1353              
1354             =cut
1355              
1356 0     0 1 0 sub tab( $self ) {
  0         0  
  0         0  
1357 0         0 $self->target->tab
1358             }
1359              
1360             =head2 C<< $mech->new_tab >>
1361              
1362             =head2 C<< $mech->new_tab_future >>
1363              
1364             my $tab2 = $mech->new_tab_future(
1365             start_url => 'https://google.com',
1366             )->get;
1367              
1368             Creates a new tab (basically, a new WWW::Mechanize::Chrome object) connected
1369             to the same Chrome session.
1370              
1371             # Use a targetInfo structure from Chrome
1372             my $tab2 = $mech->new_tab_future(
1373             tab => {
1374             'targetId' => '1F42BDF32A30700805DDC21EDB5D8C4A',
1375             },
1376             )->get;
1377              
1378             It returns a L<Future> because most event loops do not like recursing within
1379             themselves, which happens if you want to access a fresh new tab within another
1380             callback.
1381              
1382             =cut
1383              
1384 0     0 1 0 sub new_tab_future( $self, %options ) {
  0         0  
  0         0  
  0         0  
1385 0 0       0 my $new_tab = $options{ tab } ? undef : 1;
1386             return $self->new_future(
1387             %options,
1388             maybe new_tab => $new_tab,
1389             headless => $self->{headless},
1390 0         0 driver => $self->driver,
1391             driver_transport => $self->transport,
1392             );
1393             }
1394              
1395 0     0 1 0 sub new_tab( $self, %options ) {
  0         0  
  0         0  
  0         0  
1396 0         0 $self->new_tab_future( %options )->get
1397             };
1398              
1399             =head2 C<< $mech->on_popup >>
1400              
1401             my $opened;
1402             $mech->on_popup(sub( $tab_f ) {
1403             # This is a bit heavyweight, but ...
1404             $tab_f->on_done(sub($tab) {
1405             say "New window/tab was popped up:";
1406             $tab->uri_future->then(sub($uri) {
1407             say $uri;
1408             });
1409             $opened = $tab;
1410             })->retain;
1411             });
1412              
1413             $mech->click({ selector => '#popup_window' });
1414             if( $opened ) {
1415             say $opened->title;
1416             } else {
1417             say "Did not find new tab?";
1418             };
1419              
1420             Callback whenever a new tab/window gets popped up or created. The callback
1421             is handed a complete WWW::Mechanize::Chrome instance. Note that depending on
1422             your event loop, you are quite restricted on what synchronous methods you can
1423             call from within the callback.
1424              
1425             =cut
1426              
1427 0     0 1 0 sub on_popup( $self, $popup ) {
  0         0  
  0         0  
  0         0  
1428 0 0       0 if( $popup ) {
1429             # Remember all known targets, because setDiscoverTargets will list all
1430             # existing targets too :-/
1431 0         0 my %known_targets;
1432 0     0   0 my $setup = $self->transport->getTargets()->then(sub( @targets ) {
  0         0  
  0         0  
1433 0         0 %known_targets = map { $_->{targetId} => 1 } @targets;
  0         0  
1434 0         0 Future->done(1);
1435 0         0 });
1436              
1437 0     0   0 $self->{target_created} = $self->add_listener('Target.targetCreated' => sub($targetInfo) {
  0         0  
  0         0  
1438             #use Data::Dumper; warn Dumper $targetInfo;
1439 0         0 my $id = $targetInfo->{params}->{targetInfo}->{targetId};
1440 0 0 0     0 if( $targetInfo->{params}->{targetInfo}->{type} eq 'page'
1441             && ! $known_targets{ $id }
1442             ) {
1443             # use Data::Dumper; warn "--- New target"; warn Dumper $targetInfo;
1444 0         0 my $tab = $self->new_tab_future( tab => $targetInfo->{params}->{targetInfo});
1445 0         0 $popup->($tab);
1446             } else {
1447             # warn "...- already know it";
1448             };
1449 0         0 });
1450              
1451 0         0 weaken( my $s = $self );
1452             $setup->then(sub {
1453 0     0   0 $s->target->send_message('Target.setDiscoverTargets' => discover => JSON::true() )
1454 0         0 })->get;
1455             } else {
1456 0         0 $self->target->send_message('Target.setDiscoverTargets' => discover => JSON::false() )->get;
1457 0         0 delete $self->{target_created};
1458             };
1459             };
1460              
1461             sub autodie {
1462 0     0 1 0 my( $self, $val )= @_;
1463 0 0       0 $self->{autodie} = $val
1464             if @_ == 2;
1465             $_[0]->{autodie}
1466 0         0 }
1467              
1468             =head2 C<< $mech->allow( %options ) >>
1469              
1470             $mech->allow( javascript => 1 );
1471              
1472             Allow or disallow execution of Javascript
1473              
1474             =cut
1475              
1476             sub allow {
1477 0     0 1 0 my($self,%options)= @_;
1478              
1479 0         0 my @await;
1480 0 0       0 if( exists $options{ javascript } ) {
1481 0 0       0 my $disabled = !$options{ javascript } ? JSON::true : JSON::false;
1482 0         0 push @await,
1483             $self->target->send_message('Emulation.setScriptExecutionDisabled', value => $disabled );
1484             };
1485              
1486 0         0 Future->wait_all( @await )->get;
1487             }
1488              
1489             =head2 C<< $mech->emulateNetworkConditions( %options ) >>
1490              
1491             # Go offline
1492             $mech->emulateNetworkConditions(
1493             offline => JSON::true,
1494             latency => 10, # ms ping
1495             downloadThroughput => 0, # bytes/s
1496             uploadThroughput => 0, # bytes/s
1497             connectionType => 'offline', # cellular2g, cellular3g, cellular4g, bluetooth, ethernet, wifi, wimax, other.
1498             );
1499              
1500             =cut
1501              
1502 0     0 0 0 sub emulateNetworkConditions_future( $self, %options ) {
  0         0  
  0         0  
  0         0  
1503             $options{ offline } //= JSON::false,
1504             $options{ latency } //= -1,
1505             $options{ downloadThroughput } //= -1,
1506 0   0     0 $options{ uploadThroughput } //= -1,
      0        
      0        
      0        
1507             $self->target->send_message('Network.emulateNetworkConditions', %options)
1508             }
1509              
1510 0     0 1 0 sub emulateNetworkConditions( $self, %options ) {
  0         0  
  0         0  
  0         0  
1511 0         0 $self->emulateNetworkConditions_future( %options )->get
1512             }
1513              
1514             =head2 C<< $mech->setRequestInterception( @patterns ) >>
1515              
1516             $mech->setRequestInterception(
1517             { urlPattern => '*', resourceType => 'Document', interceptionStage => 'Request'},
1518             { urlPattern => '*', resourceType => 'Media', interceptionStage => 'Response'},
1519             );
1520              
1521             Sets the list of request patterns and resource types for which the interception
1522             callback will be invoked.
1523              
1524             =cut
1525              
1526 0     0 0 0 sub setRequestInterception_future( $self, @patterns ) {
  0         0  
  0         0  
  0         0  
1527 0         0 $self->target->send_message('Network.setRequestInterception', patterns => \@patterns)
1528             }
1529              
1530 0     0 1 0 sub setRequestInterception( $self, @patterns ) {
  0         0  
  0         0  
  0         0  
1531 0         0 $self->setRequestInterception_future( @patterns )->get
1532             }
1533              
1534             =head2 C<< $mech->continueInterceptedRequest( %options ) >>
1535              
1536             $mech->continueInterceptedRequest_future(
1537             interceptionId => ...
1538             );
1539              
1540             Continues an intercepted request
1541              
1542             =cut
1543              
1544 0     0 0 0 sub continueInterceptedRequest_future( $self, %options ) {
  0         0  
  0         0  
  0         0  
1545 0         0 $self->target->send_message('Network.continueInterceptedRequest', %options)
1546             }
1547              
1548 0     0 1 0 sub continueInterceptedRequest( $self, %options ) {
  0         0  
  0         0  
  0         0  
1549 0         0 $self->continueInterceptedRequest_future( %options )->get
1550             }
1551              
1552             =head2 C<< $mech->add_listener >>
1553              
1554             my $url_loaded = $mech->add_listener('Network.responseReceived', sub {
1555             my( $info ) = @_;
1556             warn "Loaded URL "
1557             . $info->{params}->{response}->{url}
1558             . ": "
1559             . $info->{params}->{response}->{status};
1560             warn "Resource timing: " . Dumper $info->{params}->{response}->{timing};
1561             });
1562              
1563             Returns a listener object. If that object is discarded, the listener callback
1564             will be removed.
1565              
1566             Calling this method in void context croaks.
1567              
1568             To see the browser console live from your Perl script, use the following:
1569              
1570             my $console = $mech->add_listener('Runtime.consoleAPICalled', sub {
1571             warn join ", ",
1572             map { $_->{value} // $_->{description} }
1573             @{ $_[0]->{params}->{args} };
1574             });
1575              
1576             If you want to explicitly remove the listener, either set it to C<undef>:
1577              
1578             undef $console;
1579              
1580             Alternatively, call
1581              
1582             $console->unregister;
1583              
1584             or call
1585              
1586             $mech->remove_listener( $console );
1587              
1588             =cut
1589              
1590 0     0 1 0 sub add_listener( $self, $event, $callback ) {
  0         0  
  0         0  
  0         0  
  0         0  
1591 0 0       0 if( ! defined wantarray ) {
1592 0         0 croak "->add_listener called in void context."
1593             . "Please store the result somewhere";
1594             };
1595 0         0 return $self->target->add_listener( $event, $callback )
1596             }
1597              
1598 0     0 0 0 sub remove_listener( $self, $listener ) {
  0         0  
  0         0  
  0         0  
1599 0         0 $listener->unregister
1600             }
1601              
1602             =head2 C<< $mech->on_request_intercepted( $cb ) >>
1603              
1604             $mech->on_request_intercepted( sub {
1605             my( $mech, $info ) = @_;
1606             warn $info->{request}->{url};
1607             $mech->continueInterceptedRequest_future(
1608             interceptionId => $info->{interceptionId}
1609             )
1610             });
1611              
1612             A callback for intercepted requests that match the patterns set up
1613             via C<setRequestInterception>.
1614              
1615             If you return a future from this callback, it will not be discarded but kept in
1616             a safe place.
1617              
1618             =cut
1619              
1620 0     0 1 0 sub on_request_intercepted( $self, $cb ) {
  0         0  
  0         0  
  0         0  
1621 0 0       0 if( $cb ) {
1622 0         0 my $s = $self;
1623 0         0 weaken $s;
1624             $self->{ on_request_intercept_listener } =
1625 0     0   0 $self->add_listener('Network.requestIntercepted', sub( $ev ) {
  0         0  
  0         0  
1626 0 0       0 if( $s->{ on_request_intercepted }) {
1627             $s->log('debug', sprintf 'Request intercepted %s: %s',
1628             $ev->{params}->{interceptionId},
1629 0         0 $ev->{params}->{request}->{url});
1630 0         0 $s->{ on_request_intercepted }->( $s, $ev->{params} );
1631             };
1632 0         0 });
1633             } else {
1634 0         0 delete $self->{ on_request_intercept_listener };
1635             };
1636 0         0 $self->{ on_request_intercepted } = $cb;
1637             }
1638              
1639             =head2 C<< $mech->searchInResponseBody( $id, %options ) >>
1640              
1641             my $request_id = ...;
1642             my @matches = $mech->searchInResponseBody(
1643             requestId => $request_id,
1644             query => 'rumpelstiltskin',
1645             caseSensitive => JSON::true,
1646             isRegex => JSON::false,
1647             );
1648             for( @matches ) {
1649             print $_->{lineNumber}, ":", $_->{lineContent}, "\n";
1650             };
1651              
1652             Returns the matches (if any) for a string or regular expression within
1653             a response.
1654              
1655             =cut
1656              
1657 0     0 0 0 sub searchInResponseBody_future( $self, %options ) {
  0         0  
  0         0  
  0         0  
1658 0         0 $self->target->send_message('Network.searchInResponseBody', %options)
1659 0     0   0 ->then(sub( $res ) {
  0         0  
1660 0         0 return Future->done( @{ $res->{result}} )
  0         0  
1661             })
1662 0         0 }
1663              
1664 0     0 1 0 sub searchInResponseBody( $self, @patterns ) {
  0         0  
  0         0  
  0         0  
1665 0         0 $self->searchInResponseBody_future( @patterns )->get
1666             }
1667              
1668             =head2 C<< $mech->on_dialog( $cb ) >>
1669              
1670             $mech->on_dialog( sub {
1671             my( $mech, $dialog ) = @_;
1672             warn $dialog->{message};
1673             $mech->handle_dialog( 1 ); # click "OK" / "yes" instead of "cancel"
1674             });
1675              
1676             A callback for Javascript dialogs (C<< alert() >>, C<< prompt() >>, ... )
1677              
1678             =cut
1679              
1680 0     0 1 0 sub on_dialog( $self, $cb ) {
  0         0  
  0         0  
  0         0  
1681 0 0       0 if( $cb ) {
1682 0         0 my $s = $self;
1683 0         0 weaken $s;
1684             $self->{ on_dialog_listener } =
1685 0     0   0 $self->add_listener('Page.javascriptDialogOpening', sub( $ev ) {
  0         0  
  0         0  
1686 0 0       0 if( $s->{ on_dialog }) {
1687 0         0 $s->log('debug', sprintf 'Javascript %s: %s', $ev->{params}->{type}, $ev->{params}->{message});
1688 0         0 $s->{ on_dialog }->( $s, $ev->{params} );
1689             };
1690 0         0 });
1691             } else {
1692 0         0 delete $self->{ on_dialog_listener };
1693             };
1694 0         0 $self->{ on_dialog } = $cb;
1695             }
1696              
1697             =head2 C<< $mech->handle_dialog( $accept, $prompt = undef ) >>
1698              
1699             $mech->on_dialog( sub {
1700             my( $mech, $dialog ) = @_;
1701             warn "[Javascript $dialog->{type}]: $dialog->{message}";
1702             $mech->handle_dialog( 1 ); # click "OK" / "yes" instead of "cancel"
1703             });
1704              
1705             Closes the current Javascript dialog.
1706              
1707             =cut
1708              
1709 0     0 1 0 sub handle_dialog( $self, $accept, $prompt = undef ) {
  0         0  
  0         0  
  0         0  
  0         0  
1710 0 0       0 my $v = $accept ? JSON::true : JSON::false;
1711 0         0 $self->log('debug', sprintf 'Dismissing Javascript dialog with %d', $accept);
1712 0 0       0 $self->target->send_message(
1713             'Page.handleJavaScriptDialog',
1714             accept => $v,
1715             promptText => (defined $prompt ? $prompt : 'generic message'),
1716             )->retain;
1717             };
1718              
1719             =head2 C<< $mech->js_console_entries() >>
1720              
1721             print $_->{type}, " ", $_->{message}, "\n"
1722             for $mech->js_console_entries();
1723              
1724             An interface to the Javascript Error Console
1725              
1726             Returns the list of entries in the JEC
1727              
1728             =cut
1729              
1730 0     0 1 0 sub js_console_entries( $self ) {
  0         0  
  0         0  
1731 0         0 @{$self->{js_events}}
  0         0  
1732             }
1733              
1734             =head2 C<< $mech->js_errors() >>
1735              
1736             print "JS error: ", $_->{message}, "\n"
1737             for $mech->js_errors();
1738              
1739             Returns the list of errors in the JEC
1740              
1741             =cut
1742              
1743             sub js_errors {
1744 0     0 1 0 my ($self) = @_;
1745 0   0     0 grep { ($_->{type} || '') ne 'log' } $self->js_console_entries
  0         0  
1746             }
1747              
1748             =head2 C<< $mech->clear_js_errors() >>
1749              
1750             $mech->clear_js_errors();
1751              
1752             Clears all Javascript messages from the console
1753              
1754             =cut
1755              
1756             sub clear_js_errors {
1757 0     0 1 0 my ($self) = @_;
1758 0         0 @{$self->{js_events}} = ();
  0         0  
1759 0         0 $self->target->send_message('Runtime.discardConsoleEntries')->get;
1760             };
1761              
1762             =head2 C<< $mech->eval_in_page( $str, %options ) >>
1763              
1764             =head2 C<< $mech->eval( $str, %options ) >>
1765              
1766             my ($value, $type) = $mech->eval( '2+2' );
1767              
1768             Evaluates the given Javascript fragment in the
1769             context of the web page.
1770             Returns a pair of value and Javascript type.
1771              
1772             This allows access to variables and functions declared
1773             "globally" on the web page.
1774              
1775             =over 4
1776              
1777             =item returnByValue
1778              
1779             If you want to create an object in Chrome and only want to keep a handle to that
1780             remote object, use C<JSON::false> for the C<returnByValue> option:
1781              
1782             my ($dummyObj,$type) = $mech->eval(
1783             'new Object',
1784             returnByValue => JSON::false
1785             );
1786              
1787             This is also helpful if the object in Chrome cannot be serialized as JSON.
1788             For example, C<window> is such an object. The return value is a hash, whose
1789             C<objectId> is the most interesting part.
1790              
1791             =back
1792              
1793             This method is special to WWW::Mechanize::Chrome.
1794              
1795             =cut
1796              
1797 0     0 1 0 sub eval_in_page($self,$str, %options) {
  0         0  
  0         0  
  0         0  
  0         0  
1798             # Report errors from scope of caller
1799             # This feels weirdly backwards here, but oh well:
1800             local @Chrome::DevToolsProtocol::CARP_NOT
1801 0         0 = (@Chrome::DevToolsProtocol::CARP_NOT, (ref $self)); # we trust this
1802             local @CARP_NOT
1803 0         0 = (@CARP_NOT, 'Chrome::DevToolsProtocol', (ref $self)); # we trust this
1804 0         0 my $result = $self->target->evaluate("$str", %options)->get;
1805              
1806 0 0       0 if( $result->{error} ) {
    0          
1807             $self->signal_condition(
1808 0         0 join "\n", grep { defined $_ }
1809             $result->{error}->{message},
1810             $result->{error}->{data},
1811             $result->{error}->{code}
1812 0         0 );
1813             } elsif( $result->{exceptionDetails} ) {
1814             $self->signal_condition(
1815 0         0 join "\n", grep { defined $_ }
1816             $result->{exceptionDetails}->{text},
1817             $result->{exceptionDetails}->{exception}->{description},
1818 0         0 );
1819             }
1820              
1821 0 0       0 if( exists $result->{result}->{value}) {
1822 0         0 return $result->{result}->{value}, $result->{result}->{type};
1823             } else {
1824 0         0 return $result->{result}, $result->{result}->{type};
1825             }
1826             };
1827              
1828             {
1829 68     68   608 no warnings 'once';
  68         178  
  68         41075  
1830             *eval = \&eval_in_page;
1831             }
1832              
1833             =head2 C<< $mech->eval_in_chrome $code, @args >>
1834              
1835             $mech->eval_in_chrome(<<'JS', "Foobar/1.0");
1836             this.settings.userAgent= arguments[0]
1837             JS
1838              
1839             Evaluates Javascript code in the context of Chrome.
1840              
1841             This allows you to modify properties of Chrome.
1842              
1843             This is currently not implemented.
1844              
1845             =cut
1846              
1847             sub eval_in_chrome {
1848 0     0 1 0 my ($self, $code, @args) = @_;
1849 0         0 croak "Can't call eval_in_chrome";
1850             };
1851              
1852             =head2 C<< $mech->callFunctionOn( $function, @arguments ) >>
1853              
1854             my ($value, $type) = $mech->callFunctionOn(
1855             'function(greeting) { window.alert(greeting)}',
1856             objectId => $someObjectId,
1857             arguments => [{ value => 'Hello World' }]
1858             );
1859              
1860             Runs the given function with the specified arguments. This is the only way to
1861             pass arguments to a function call without doing risky string interpolation.
1862             The Javascript C<this> object will be set to the object referenced from the
1863             C<objectId>.
1864              
1865             The C<arguments> option expects an arrayref of hashrefs. Each hash describes one
1866             function argument.
1867              
1868             The C<objectId> parameter is optional. Leaving out the C<objectId> parameter
1869             will create a dummy object on which the function then is called.
1870              
1871             This method is special to WWW::Mechanize::Chrome.
1872              
1873             =cut
1874              
1875 0     0 0 0 sub callFunctionOn_future( $self, $str, %options ) {
  0         0  
  0         0  
  0         0  
  0         0  
1876             # Report errors from scope of caller
1877             # This feels weirdly backwards here, but oh well:
1878             local @Chrome::DevToolsProtocol::CARP_NOT
1879 0         0 = (@Chrome::DevToolsProtocol::CARP_NOT, (ref $self)); # we trust this
1880             local @CARP_NOT
1881 0         0 = (@CARP_NOT, 'Chrome::DevToolsProtocol', (ref $self)); # we trust this
1882              
1883 0         0 my $objId;
1884 0 0       0 if( ! $options{ objectId }) {
1885 0         0 $objId = $self->target->evaluate('new Object',
1886             returnByValue => JSON::false
1887 0     0   0 )->then(sub($result) {
  0         0  
1888 0         0 return Future->done( $result->{result}->{objectId});
1889 0         0 });
1890             } else {
1891 0         0 $objId = Future->done( $options{ objectId });
1892             };
1893              
1894 0     0   0 $objId->then( sub( $objectId ) {
  0         0  
  0         0  
1895 0         0 $options{ objectId } = $objectId;
1896 0         0 $self->target->callFunctionOn($str, %options)
1897 0     0   0 })->then( sub( $result ) {
  0         0  
  0         0  
1898              
1899 0 0       0 if( $result->{error} ) {
    0          
1900             $self->signal_condition(
1901 0         0 join "\n", grep { defined $_ }
1902             $result->{error}->{message},
1903             $result->{error}->{data},
1904             $result->{error}->{code}
1905 0         0 );
1906             } elsif( $result->{exceptionDetails} ) {
1907             $self->signal_condition(
1908 0         0 join "\n", grep { defined $_ }
1909             $result->{exceptionDetails}->{text},
1910             $result->{exceptionDetails}->{exception}->{description},
1911 0         0 );
1912             }
1913 0 0       0 if( exists $result->{result}->{value}) {
1914 0         0 return Future->done( $result->{result}->{value}, $result->{result}->{type} );
1915             } else {
1916 0         0 return Future->done( $result->{result}, $result->{result}->{type} );
1917             }
1918             })
1919 0         0 };
1920              
1921             sub callFunctionOn {
1922 0     0 1 0 my ($self,$str, %options) = @_;
1923             # Report errors from scope of caller
1924             # This feels weirdly backwards here, but oh well:
1925             local @Chrome::DevToolsProtocol::CARP_NOT
1926 0         0 = (@Chrome::DevToolsProtocol::CARP_NOT, (ref $self)); # we trust this
1927             local @CARP_NOT
1928 0         0 = (@CARP_NOT, 'Chrome::DevToolsProtocol', (ref $self)); # we trust this
1929 0         0 $self->callFunctionOn_future($str, %options)->get;
1930             };
1931              
1932             {
1933 68     68   636 no warnings 'once';
  68         256  
  68         366598  
1934             *eval = \&eval_in_page;
1935             }
1936              
1937 0     0 0 0 sub agent_future( $self, $ua ) {
  0         0  
  0         0  
  0         0  
1938 0         0 $self->target->send_message('Network.setUserAgentOverride', userAgent => $ua )
1939             }
1940              
1941 0     0 0 0 sub agent( $self, $ua ) {
  0         0  
  0         0  
  0         0  
1942 0 0       0 if( $ua ) {
1943 0         0 $self->agent_future( $ua )->get;
1944             };
1945              
1946 0         0 $self->chrome_version_info->{"User-Agent"}
1947             }
1948              
1949             =head2 C<< ->autoclose_tab >>
1950              
1951             Set the C<autoclose> option
1952              
1953             =cut
1954              
1955 0     0 1 0 sub autoclose_tab( $self, $autoclose ) {
  0         0  
  0         0  
  0         0  
1956 0         0 $self->{autoclose} = $autoclose
1957             }
1958              
1959             =head2 C<< ->close >>
1960              
1961             $mech->close()
1962              
1963             Tear down all connections and shut down Chrome.
1964              
1965             =cut
1966              
1967             sub close {
1968 1     1 1 3 my $pid= delete $_[0]->{pid};
1969              
1970             #if( $_[0]->{autoclose} and $_[0]->tab and my $tab_id = $_[0]->tab->{id} ) {
1971             # $_[0]->target->close_tab({ id => $tab_id })->get();
1972             #};
1973 1 50 33     8 if( $_[0]->{autoclose} and $_[0]->target and $_[0]->tab ) {
      33        
1974 0         0 $_[0]->target->close->retain();
1975             #$_[0]->target->close->get(); # just to see if there is an error
1976             };
1977              
1978             #if( $pid and $_[0]->{cached_version} > 65) {
1979             # # Try a graceful shutdown
1980             # $_[0]->target->send_message('Browser.close' )->get
1981             #};
1982              
1983 1         13 local $@;
1984 1         3 eval {
1985             # Shut down our websocket connection
1986 1 50       3 if( $_[0]->{ driver }) {
1987             # This ruins too much of our infrastructure
1988             # We want to keep the connection open and maybe only call
1989             # ->close() from their DESTROY?!
1990             #$_[0]->{ driver }->close
1991             };
1992             };
1993 1         3 delete $_[0]->{ driver };
1994              
1995 1 50 33     14 if( $_[0]->{autoclose} and $_[0]->{kill_pid} ) {
1996 0         0 $_[0]->kill_child( $_[0]->{cleanup_signal}, $pid, $_[0]->{wait_file} );
1997             }
1998             }
1999              
2000 0     0 0 0 sub kill_child( $self, $signal, $pid, $wait_file ) {
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
2001 0 0 0     0 if( $pid and kill 0 => $pid) {
2002 0         0 local $SIG{CHLD} = 'IGNORE';
2003 0         0 undef $!;
2004 0 0       0 if( ! kill $signal => $pid ) {
2005             # The child already has gone away?!
2006 0         0 warn "Couldn't kill browser child process $pid with $_[0]->{cleanup_signal}: $!";
2007             # Gobble up any exit status
2008 0         0 warn waitpid -1, WNOHANG;
2009             } else {
2010              
2011 0 0       0 if( $^O =~ /darwin/i ) {
2012             # Busy-wait until the kid has gone away since on OSX this caused
2013             # infinite hangs at least on Travis CI !?
2014 0         0 my $timeout = time+2;
2015 0         0 while( time < $timeout ) {
2016 0         0 my $res = waitpid $pid, WNOHANG;
2017 0 0 0     0 if( $res != -1 and $res != $pid ) {
2018 0 0       0 warn "Couldn't wait for child '$pid' ($res)?"
2019             if $res != 0;
2020 0         0 sleep 0.1;
2021             } else {
2022 0         0 last;
2023             };
2024             };
2025             } else {
2026             # on Linux and Windows, plain waitpid Just Works
2027 0         0 waitpid $pid, 0;
2028             # but still, check again that the child has really gone away:
2029 0         0 my $timeout = time+2;
2030 0         0 while( time < $timeout ) {
2031 0         0 my $res = kill 0 => $pid;
2032 0 0       0 if( $res ) {
2033 0         0 sleep 0.1;
2034             } else {
2035 0         0 last;
2036             };
2037             };
2038              
2039             };
2040             };
2041              
2042 0 0       0 if( my $path = $wait_file) {
2043 0         0 my $timeout = time + 10;
2044 0         0 while( time < $timeout ) {
2045 0 0       0 last unless(-e $path);
2046 0 0       0 unlink($path) and last;
2047 0         0 $self->sleep(0.1);
2048             }
2049             };
2050             };
2051             }
2052              
2053             sub DESTROY {
2054             #warn "Closing mechanize";
2055 1     1   15 $_[0]->close();
2056 1         3 %{ $_[0] }= (); # clean out all other held references
  1         11  
2057             }
2058              
2059             =head2 C<< $mech->list_tabs >>
2060              
2061             my @open_tabs = $mech->list_tabs()->get;
2062             say $open_tabs[0]->{title};
2063              
2064             Returns the open tabs as a list of hashrefs.
2065              
2066             =cut
2067              
2068 0     0 1   sub list_tabs( $self ) {
  0            
  0            
2069 0           $self->transport->getTargets;
2070             }
2071              
2072             =head2 C<< $mech->highlight_node( @nodes ) >>
2073              
2074             my @links = $mech->selector('a');
2075             $mech->highlight_node(@links);
2076             print $mech->content_as_png();
2077              
2078             Convenience method that marks all nodes in the arguments
2079             with a red frame.
2080              
2081             This is convenient if you need visual verification that you've
2082             got the right nodes.
2083              
2084             =cut
2085              
2086 0     0 0   sub highlight_nodes($self, @nodes) {
  0            
  0            
  0            
2087 0           foreach my $node (@nodes) {
2088 0           $self->callFunctionOn(
2089             'function() {
2090             if( "none" == this.style.display ) {
2091             this.style.display= "block";
2092             };
2093             this.style.backgroundColor = "red";
2094             this.style.border = "solid black 1px"
2095             }',
2096             objectId => $node->objectId,
2097             arguments => []
2098             );
2099             }
2100             }
2101              
2102             =head1 NAVIGATION METHODS
2103              
2104             =head2 C<< $mech->get( $url, %options ) >>
2105              
2106             my $response = $mech->get( $url );
2107              
2108             Retrieves the URL C<URL>.
2109              
2110             It returns a L<HTTP::Response> object for interface compatibility
2111             with L<WWW::Mechanize>.
2112              
2113             Note that the returned L<HTTP::Response> object gets the response body
2114             filled in lazily, so you might have to wait a moment to get the response
2115             body from the result. This is a premature optimization and later releases of
2116             WWW::Mechanize::Chrome are planned to fetch the response body immediately when
2117             accessing the response body.
2118              
2119             Note that Chrome does not support download of files through the API.
2120              
2121             =head3 Options
2122              
2123             =over 4
2124              
2125             =item *
2126              
2127             C<intrapage> - Override the detection of whether to wait for a HTTP response
2128             or not. Setting this will never wait for an HTTP response.
2129              
2130             =back
2131              
2132             =cut
2133              
2134 0     0 0   sub update_response($self, $response) {
  0            
  0            
  0            
2135 0           $self->log('trace', 'Updated response object');
2136 0           $self->invalidate_cached_values;
2137 0           $self->{response} = $response;
2138             }
2139              
2140             =head2 C<< $mech->_collectEvents >>
2141              
2142             my $events = $mech->_collectEvents(
2143             sub { $_[0]->{method} eq 'Page.loadEventFired' }
2144             );
2145             my( $e,$r) = Future->wait_all( $events, $self->target->send_message(...));
2146              
2147             Internal method to create a Future that waits for an event that is sent by Chrome.
2148              
2149             The subroutine is the predicate to check to see if the current event
2150             is the event we have been waiting for.
2151              
2152             The result is a Future that will return all captured events.
2153              
2154             =cut
2155              
2156 0     0     sub _collectEvents( $self, @info ) {
  0            
  0            
  0            
2157             # Read the stuff that the driver sends to us:
2158 0           my $predicate = pop @info;
2159 0 0         ref $predicate eq 'CODE'
2160             or die "Need a predicate as the last parameter, not '$predicate'!";
2161              
2162 0           my @events = ();
2163 0           my $done = $self->target->future;
2164 0           my $s = $self;
2165 0           weaken $s;
2166 0     0     $self->target->on_message( sub( $message ) {
  0            
  0            
2167 0           push @events, $message;
2168 0 0         if( $predicate->( $events[-1] )) {
2169 0           my $frameId = $events[-1]->{params}->{frameId};
2170 0   0       $s->log( 'debug', "Received final message, unwinding", sprintf "(%s)", $frameId || '-');
2171 0           $s->log( 'trace', "Received final message, unwinding", $events[-1] );
2172 0           $s->target->on_message( undef );
2173 0           $done->done( @info, @events );
2174             };
2175 0           });
2176 0           $done
2177             }
2178              
2179 0     0     sub _fetchFrameId( $self, $ev ) {
  0            
  0            
  0            
2180 0 0 0       if( $ev->{method} eq 'Page.frameStartedLoading'
      0        
2181             || $ev->{method} eq 'Page.frameScheduledNavigation'
2182             || $ev->{method} eq 'Network.requestWillBeSent'
2183             ) {
2184 0           my $frameId = $ev->{params}->{frameId};
2185 0   0       $self->log('debug', sprintf "Found frame id as %s", $frameId || '-');
2186 0           return ($frameId);
2187             }
2188             };
2189              
2190 0     0     sub _fetchRequestId( $self, $ev ) {
  0            
  0            
  0            
2191 0 0 0       if( $ev->{method} eq 'Page.frameStartedLoading'
      0        
2192             || $ev->{method} eq 'Page.frameScheduledNavigation'
2193             || $ev->{method} eq 'Network.requestWillBeSent'
2194             ) {
2195 0           my $requestId = $ev->{params}->{requestId};
2196 0 0         if( $requestId ) {
2197 0           $self->log('debug', sprintf "Found request id as %s", $requestId);
2198 0           return ($requestId);
2199             } else {
2200             return
2201 0           };
2202             }
2203             };
2204              
2205 0     0     sub _waitForNavigationEnd( $self, %options ) {
  0            
  0            
  0            
2206             # Capture all events as we seem to have initiated some network transfers
2207             # If we see a Page.frameScheduledNavigation then Chrome started navigating
2208             # to a new page in response to our click and we should wait until we
2209             # received all the navigation events.
2210              
2211 0   0       my $frameId = $options{ frameId } || $self->frameId;
2212 0   0       my $requestId = $options{ requestId } || $self->requestId;
2213              
2214             # Actually, we need to wait for DOM.documentUpdated!
2215              
2216 0   0       my $msg = sprintf "Capturing events until 'Page.frameStoppedLoading' or 'Page.frameClearedScheduledNavigation' for frame %s",
2217             $frameId || '-';
2218 0 0         $msg .= " or 'Network.loadingFailed' or 'Network.loadingFinished' for request '$requestId'"
2219             if $requestId;
2220              
2221 0           $self->log('debug', $msg);
2222              
2223 0           my $s = $self;
2224 0           weaken $s;
2225 0     0     my $events_f = $self->_collectEvents( sub( $ev ) {
  0            
  0            
2226 0 0         if( ! $ev->{method}) {
2227             # We get empty responses when talking to indirect targets
2228             return
2229 0           };
2230              
2231             # Let's assume that the first frame id we see is "our" frame
2232 0   0       $frameId ||= $s->_fetchFrameId($ev);
2233 0   0       $requestId ||= $s->_fetchRequestId($ev);
2234              
2235             my $stopped = ( $ev->{method} eq 'Page.frameStoppedLoading'
2236             && $ev->{params}->{frameId} eq $frameId)
2237             ||
2238             ( $ev->{method} eq 'Network.loadingFinished'
2239             && (! $ev->{params}->{frameId} || $ev->{params}->{frameId} eq ($frameId || ''))
2240 0   0       && (! $ev->{params}->{requestId} || $ev->{params}->{requestId} eq ($requestId || ''))
2241             );
2242             # This means basically no navigation events will follow:
2243             my $internal_navigation = ( $ev->{method} eq 'Page.navigatedWithinDocument'
2244             && $requestId
2245             && (! exists $ev->{params}->{requestId}
2246 0   0       or ($ev->{params}->{requestId} eq $requestId)));
2247             $internal_navigation ||= ( $ev->{method} eq 'Page.frameClearedScheduledNavigation'
2248 0   0       && $ev->{params}->{frameId} eq $frameId);
      0        
2249              
2250             # This is far too early, but some requests only send this?!
2251             # Maybe this can be salvaged by setting a timeout when we see this?!
2252             my $domcontent = ( 0 # $options{ just_request }
2253             #&& $ev->{method} eq 'Page.domContentEventFired', # this should be the only one we need (!)
2254             # but we never learn which page (!). So this does not play well with iframes :(
2255 0           && $ev->{method} eq 'DOM.documentUpdated', # this should be the only one we need (!)
2256             # but we never learn which page (!). So this does not play well with iframes :(
2257             );
2258              
2259             my $failed = ( $ev->{method} eq 'Network.loadingFailed'
2260             && $requestId
2261 0   0       && $ev->{params}->{requestId} eq $requestId);
2262             my $download= ( $ev->{method} eq 'Network.responseReceived'
2263             && $requestId
2264             && $ev->{params}->{requestId} eq $requestId
2265             && exists $ev->{params}->{response}->{headers}->{"Content-Disposition"}
2266 0   0       && $ev->{params}->{response}->{headers}->{"Content-Disposition"} =~ m!^attachment\b!
2267             );
2268 0   0       return $stopped || $internal_navigation || $failed || $download; # $domcontent;
2269 0           });
2270              
2271 0           $events_f;
2272             }
2273              
2274 0     0     sub _mightNavigate( $self, $get_navigation_future, %options ) {
  0            
  0            
  0            
  0            
2275 0           undef $self->{frameId};
2276 0           undef $self->{requestId};
2277 0           my $frameId = $options{ frameId };
2278 0           my $requestId = $options{ requestId };
2279              
2280 0           my $scheduled = $self->target->one_shot(
2281             'Page.frameScheduledNavigation',
2282             'Page.frameStartedLoading',
2283             'Network.requestWillBeSent', # trial
2284             #'Page.frameResized', # download
2285             'Inspector.detached', # Browser (window) was closed by user
2286             'Page.navigatedWithinDocument',
2287             );
2288 0           my $navigated;
2289             my $does_navigation;
2290 0           my $target_url = $options{ url };
2291              
2292             {
2293 0           my $s = $self;
  0            
2294 0           weaken $s;
2295 0           $does_navigation = $scheduled
2296 0     0     ->then(sub( $ev ) {
  0            
2297 0           my $res;
2298 0 0 0       if( $ev->{method} eq 'Page.frameResized'
    0          
    0          
2299 0           and 0+keys %{ $ev->{params} } == 0 ) {
2300             # This is dead code that is never reached (see above)
2301             # Chrome v64 doesn't indicate at all to the API that a
2302             # download started :-(
2303             # Also, we won't know that it finished, or what name the
2304             # file got
2305             # At least unless we try to parse that from the response body :(
2306 0           $s->log('trace', "Download started, returning synthesized event");
2307 0           $navigated++;
2308 0           $s->{ frameId } = $ev->{params}->{frameId};
2309             $res = Future->done(
2310             # Since Chrome v64,
2311             { method => 'MechanizeChrome.download', params => {
2312             frameId => $ev->{params}->{frameId},
2313             loaderId => $ev->{params}->{loaderId},
2314 0           response => {
2315             status => 200,
2316             statusText => 'faked response',
2317             headers => {
2318             'Content-Disposition' => 'attachment; filename=unknown',
2319             }
2320             }}
2321             })
2322              
2323             } elsif( $ev->{method} eq 'Inspector.detached' ) {
2324 0           $s->log('error', "Inspector was detached");
2325 0           $res = Future->fail("Inspector was detached");
2326              
2327             } elsif( $ev->{method} eq 'Page.navigatedWithinDocument' ) {
2328 0           $s->log('trace', "Intra-page navigation started, logging ($ev->{method})");
2329 0   0       $frameId ||= $s->_fetchFrameId( $ev );
2330             $res = Future->done(
2331             # Since Chrome v64,
2332             { method => 'Page.intra-page-navigation', params => {
2333             frameId => $ev->{params}->{frameId},
2334             loaderId => $ev->{params}->{loaderId},
2335 0           response => {
2336             status => 200,
2337             statusText => 'faked response',
2338             }}
2339             })
2340              
2341             } else {
2342 0           $s->log('trace', "Navigation started, logging ($ev->{method})");
2343 0           $navigated++;
2344              
2345 0   0       $frameId ||= $s->_fetchFrameId( $ev );
2346 0   0       $requestId ||= $s->_fetchRequestId( $ev );
2347 0           $s->{ frameId } = $frameId;
2348 0           $s->{ requestId } = $requestId;
2349              
2350 0           $res = $s->_waitForNavigationEnd( %options )
2351             };
2352 0           return $res
2353 0           });
2354             };
2355              
2356             # Kick off the navigation ourselves
2357 0           my $s = $self;
2358 0           weaken $s;
2359              
2360 0           my $nav;
2361             $get_navigation_future->()
2362             ->then( sub {
2363 0     0     $nav = $_[0];
2364              
2365             # We have a race condition to find out whether Chrome navigates or not
2366             # so we wait a bit to see if it will navigate in response to our click
2367 0           $s->sleep_future(0.1); # X XX baad fix
2368             })->then( sub {
2369 0     0     my $f;
2370             my @events;
2371 0 0 0       if( !$options{ intrapage } and $navigated ) {
2372             $f = $does_navigation->then( sub {
2373 0           @events = @_;
2374             # Handle all the events, by turning them into a ->response again
2375 0           my $res = $self->httpMessageFromEvents( $self->frameId, \@events, $target_url );
2376 0           $self->update_response( $res );
2377 0           $scheduled->cancel;
2378 0           undef $scheduled;
2379              
2380             # Store our frame id so we know what events to listen for in the future!
2381 0   0       $self->{frameId} ||= $nav->{frameId};
2382              
2383 0           Future->done( \@events )
2384             })
2385 0           } else {
2386 0           $self->log('trace', "No navigation occurred, not collecting events");
2387 0           $does_navigation->cancel;
2388 0           $f = Future->done(\@events);
2389 0           $scheduled->cancel;
2390 0           undef $scheduled;
2391             };
2392              
2393 0           return $f
2394             })
2395 0           }
2396              
2397 0     0 0   sub get_future($self, $url, %options ) {
  0            
  0            
  0            
  0            
2398              
2399             # $frameInfo might come _after_ we have already seen messages for it?!
2400             # So we need to capture all events even before we send our command to the
2401             # browser, as we might receive messages before we receive the answer to
2402             # our command:
2403 0           my $s = $self;
2404 0           weaken $s;
2405             my $events = $self->_mightNavigate( sub {
2406 0     0     $s->log('debug', "Navigating to [$url]");
2407 0           $s->target->send_message(
2408             'Page.navigate',
2409             url => "$url"
2410             )
2411             }, url => "$url", %options, navigates => 1 )
2412             ->then( sub {
2413 0     0     $s->invalidate_cached_values;
2414 0           Future->done( $s->response )
2415             })
2416 0           };
2417              
2418 0     0 1   sub get($self, $url, %options ) {
  0            
  0            
  0            
  0            
2419              
2420 0           $self->get_future($url, %options)->get;
2421             };
2422              
2423             =head2 C<< $mech->get_local( $filename , %options ) >>
2424              
2425             $mech->get_local('test.html');
2426              
2427             Shorthand method to construct the appropriate
2428             C<< file:// >> URI and load it into Chrome. Relative
2429             paths will be interpreted as relative to C<$0>
2430             or the C<basedir> option.
2431              
2432             This method accepts the same options as C<< ->get() >>.
2433              
2434             This method is special to WWW::Mechanize::Chrome but could
2435             also exist in WWW::Mechanize through a plugin.
2436              
2437             B<Warning>: Chrome does not handle local files well. Especially
2438             subframes do not get loaded properly.
2439              
2440             =cut
2441              
2442 0     0     sub _local_url( $self, $htmlfile, %options ) {
  0            
  0            
  0            
  0            
2443 0           my $basedir;
2444 0 0         if( exists $options{ basedir }) {
2445 0           $basedir = $options{ basedir };
2446             } else {
2447 0           require Cwd;
2448 0           require File::Spec;
2449 0           $basedir = dirname($0);
2450             };
2451              
2452 0           my $fn = File::Spec->rel2abs( $htmlfile, $basedir );
2453 0           $fn =~ s!\\!/!g; # fakey "make file:// URL"
2454 0           my $url;
2455 0 0         if( $^O =~ /mswin/i ) {
2456 0           $url= "file:///$fn";
2457             } else {
2458 0           $url= "file://$fn";
2459             };
2460 0           return $url
2461             }
2462              
2463 0     0 1   sub get_local( $self, $htmlfile, %options ) {
  0            
  0            
  0            
  0            
2464 0           my $url = $self->_local_url( $htmlfile, %options );
2465 0           my $res = $self->get($url, %options);
2466             ## Chrome is not helpful with its error messages for local URLs
2467             #if( 0+$res->headers->header_field_names and ([$res->headers->header_field_names]->[0] ne 'x-www-mechanize-Chrome-fake-success' or $self->uri ne 'about:blank')) {
2468             # # We need to fake the content headers from <meta> tags too...
2469             # # Maybe this even needs to go into ->get()
2470             # $res->code( 200 );
2471             #} else {
2472             # $res->code( 400 ); # Must have been "not found"
2473             #};
2474 0           $res
2475             }
2476              
2477 0     0 0   sub httpRequestFromChromeRequest( $self, $event ) {
  0            
  0            
  0            
2478             my $req = HTTP::Request->new(
2479             $event->{params}->{request}->{method},
2480             $event->{params}->{request}->{url},
2481 0           HTTP::Headers->new( %{ $event->{params}->{request}->{headers}} ),
  0            
2482             );
2483             };
2484              
2485             =head2 C<< $mech->getRequestPostData >>
2486              
2487             if( $info->{params}->{response}->{requestHeaders}->{":method"} eq 'POST' ) {
2488             $req->{postBody} = $m->getRequestPostData( $id );
2489             };
2490              
2491             Retrieves the data sent with a POST request
2492              
2493             =cut
2494              
2495 0     0 0   sub getRequestPostData_future( $self, $requestId ) {
  0            
  0            
  0            
2496 0           $self->log('debug', "Fetching request POST body for $requestId");
2497 0           weaken( my $s = $self );
2498             return
2499             $self->target->send_message('Network.getRequestPostData', requestId => $requestId)
2500             ->then(sub {
2501 0     0     $s->log('trace', "Have POST body", @_);
2502 0           my ($body_obj) = @_;
2503              
2504 0           my $body = $body_obj->{postData};
2505             # WTF? The documentation says the body is base64 encoded, but
2506             # experimentation shows it isn't, at least for JSON content :-/
2507             #$body = decode_base64( $body );
2508 0           Future->done( $body )
2509 0           });
2510             }
2511              
2512 0     0 1   sub getRequestPostData( $self, $requestId ) {
  0            
  0            
  0            
2513 0           $self->getRequestPostData_future( $requestId )->get
2514             }
2515              
2516 0     0 0   sub getResponseBody( $self, $requestId ) {
  0            
  0            
  0            
2517 0           $self->log('debug', "Fetching response body for $requestId");
2518 0           my $s = $self;
2519 0           weaken $s;
2520              
2521 0           $self->{__responseInFlight} = 1;
2522              
2523             return
2524             $self->target->send_message('Network.getResponseBody', requestId => $requestId)
2525             ->then(sub {
2526 0     0     $s->log('debug', "Have body", @_);
2527 0           my ($body_obj) = @_;
2528              
2529 0           $s->invalidate_cached_values;
2530              
2531 0           delete $s->{__responseInFlight};
2532              
2533 0           my $body = $body_obj->{body};
2534             $body = decode_base64( $body )
2535 0 0         if $body_obj->{base64Encoded};
2536 0           Future->done( $body )
2537 0           });
2538             }
2539              
2540 0     0 0   sub httpResponseFromChromeResponse( $self, $res ) {
  0            
  0            
  0            
2541             my $response = HTTP::Response->new(
2542             $res->{params}->{response}->{status} || 200, # is 0 for files?!
2543             $res->{params}->{response}->{statusText},
2544 0   0       HTTP::Headers->new( %{ $res->{params}->{response}->{headers} }),
  0            
2545             );
2546 0           $self->log('debug',sprintf "Status %0d - %s",$response->code, $response->status_line);
2547              
2548             # Also fetch the response body and include it in the response
2549             # as we can't do that lazily...
2550             # This is nasty, as we will fill in the response lazily and the user has
2551             # no way of knowing when we have filled in the response body
2552             # The proper way might be to return a proxy object...
2553 0           my $requestId = $res->{params}->{requestId};
2554              
2555 0 0         if( $requestId ) {
2556 0           my $full_response_future;
2557              
2558 0           my $s = $self;
2559 0           weaken $s;
2560 0     0     $full_response_future = $self->getResponseBody( $requestId )->then( sub( $body ) {
  0            
  0            
2561 0           $s->log('debug', "Response body arrived");
2562              
2563             # We need to encode the body back to the appropriate bytes:
2564 0           my $ct = $response->content_type;
2565              
2566 0   0       $ct ||= 'text/plain';
2567              
2568 0 0         if( $ct =~ m!^text/(\w+); charset=(.*?)! ) {
2569 0           warn "Re-encoding back to $2";
2570 0           $body = encode( "$2", $body );
2571             } else {
2572             # assume Latin-1 (actually, strip the encoding information from the Perl string)
2573 0           $body = encode( 'Latin-1', $body );
2574             };
2575              
2576 0           $response->content( $body );
2577             #undef $full_response_future;
2578 0           Future->done($body)
2579 0           })->retain;
2580             #$response->content_ref( \$body );
2581             };
2582 0           $response
2583             };
2584              
2585 0     0 0   sub httpResponseFromChromeNetworkFail( $self, $res ) {
  0            
  0            
  0            
2586             my $response = HTTP::Response->new(
2587             $res->{params}->{response}->{status} || 599, # No error code exists for files
2588             $res->{params}->{response}->{errorText},
2589 0   0       HTTP::Headers->new(),
2590             );
2591             };
2592              
2593 0     0 0   sub httpResponseFromChromeUrlUnreachable( $self, $res ) {
  0            
  0            
  0            
2594             my $response = HTTP::Response->new(
2595             599, # No error code exists for files
2596             "Unreachable URL: " . $res->{params}->{frame}->{unreachableUrl},
2597 0           HTTP::Headers->new(),
2598             );
2599             };
2600              
2601 0     0 0   sub httpMessageFromEvents( $self, $frameId, $events, $url ) {
  0            
  0            
  0            
  0            
  0            
2602 0           my ($requestId,$loaderId);
2603              
2604 0 0         if( $url ) {
2605             # Find the request id of the request
2606 0           for( @$events ) {
2607 0 0         next unless $_->{method};
2608 0 0 0       if( defined $frameId
      0        
2609             and $_->{method} eq 'Network.requestWillBeSent'
2610             and $_->{params}->{frameId} eq $frameId ) {
2611 0 0 0       if( $url and $_->{params}->{request}->{url} eq $url ) {
2612 0           $requestId = $_->{params}->{requestId};
2613             } else {
2614 0   0       $requestId ||= $_->{params}->{requestId};
2615             };
2616             }
2617             };
2618             };
2619              
2620             # Just silence some undef warnings
2621 0 0         if( ! defined $requestId) {
2622 0           $requestId = ''
2623             };
2624 0 0         if( ! defined $frameId) {
2625 0           $frameId = ''
2626             };
2627              
2628             my @events = grep {
2629             my $this_frame = (exists $_->{params}->{frameId} && $_->{params}->{frameId})
2630 0   0       || (exists $_->{params}->{frame}->{id} && $_->{params}->{frame}->{id});
2631 0 0 0       if( exists $_->{params}->{requestId}
    0 0        
2632             and $_->{params}->{requestId} eq $requestId
2633             ) {
2634 0           "Matches our request id"
2635             } elsif( ! exists $_->{params}->{requestId}
2636             and $this_frame eq $frameId
2637             ) {
2638 0           "Matches our frame id and has no associated request"
2639             } else {
2640 0           ""
2641             }
2642              
2643             } map {
2644             # Extract the loaderId and requestId, if we haven't found it yet
2645 0   0       my $fi = $frameId || '';
  0            
2646 0   0       my $rfi = $_->{params}->{frameId} || '';
2647 0 0 0       if( $_->{method} eq 'Network.requestWillBeSent' and $rfi eq $fi ) {
2648 0   0       $requestId ||= $_->{params}->{requestId};
2649 0   0       $loaderId ||= $_->{params}->{loaderId};
2650 0   0       $requestId ||= $_->{params}->{requestId};
2651             };
2652 0           $_
2653             } @$events;
2654              
2655 0           my %events;
2656 0           for (@events) {
2657             #warn join " - ", $_->{method}, $_->{params}->{loaderId}, $_->{params}->{frameId};
2658 0   0       $events{ $_->{method} } ||= $_;
2659             };
2660              
2661             # Create HTTP::Request object from 'Network.requestWillBeSent'
2662 0           my $request;
2663             my $response;
2664              
2665             my $about_blank_loaded = $events{ "Page.frameNavigated" }
2666 0   0       && $events{ "Page.frameNavigated" }->{params}->{frame}->{url} eq 'about:blank';
2667 0 0 0       if( $about_blank_loaded ) {
    0 0        
    0 0        
    0 0        
    0          
    0          
    0          
    0          
    0          
2668             #warn "About:blank";
2669 0           $response = HTTP::Response->new(
2670             200,
2671             'OK',
2672             );
2673             } elsif ( my $res = $events{ 'Network.responseReceived' }) {
2674             #warn "Network.responseReceived";
2675 0           $response = $self->httpResponseFromChromeResponse( $res );
2676 0           $response->request( $request );
2677              
2678             } elsif ( $res = $events{ 'Page.navigatedWithinDocument' }) {
2679             # A fake response, just in case anybody checks
2680 0           $response = HTTP::Response->new(
2681             200, # is 0 for files?!
2682             "OK",
2683             HTTP::Headers->new(),
2684             );
2685 0           $response->request( $request );
2686              
2687             } elsif( $res = $events{ 'Network.loadingFailed' }) {
2688             #warn "Network.loadingFailed";
2689 0           $response = $self->httpResponseFromChromeNetworkFail( $res );
2690 0           $response->request( $request );
2691              
2692             } elsif ( $res = $events{ 'Page.frameNavigated' }
2693             and $res->{params}->{frame}->{unreachableUrl}) {
2694             #warn "Network.frameNavigated (unreachable)";
2695 0           $response = $self->httpResponseFromChromeUrlUnreachable( $res );
2696 0           $response->request( $request );
2697              
2698             } elsif ( $res = $events{ 'Page.frameNavigated' }
2699             and $res->{params}->{frame}->{url} =~ m!^file://!) {
2700             #warn "Network.frameNavigated (file)";
2701             # Chrome v67+ doesn't send network events for file:// navigation
2702 0           $response = HTTP::Response->new(
2703             200, # is 0 for files?!
2704             "OK",
2705             HTTP::Headers->new(),
2706             );
2707 0           $response->request( $request );
2708              
2709             # Popup window, handled in a new instance, if captured
2710             } elsif ( $res = $events{ 'Page.frameClearedScheduledNavigation' }
2711             and $res->{params}->{frameId} eq $frameId) {
2712             #warn "Network.frameNavigated (file)";
2713 0           $response = HTTP::Response->new(
2714             200, # is 0 for files?!
2715             "OK",
2716             HTTP::Headers->new(),
2717             );
2718 0           $response->request( $request );
2719              
2720              
2721             } elsif ( $res = $events{ 'Page.frameStoppedLoading' }
2722             and $res->{params}->{frameId} eq $frameId) {
2723             #warn "Network.frameStoppedLoading";
2724             # Chrome v67+ doesn't send network events for file:// navigation
2725             # so we need to fake it completely
2726 0           $response = HTTP::Response->new(
2727             200, # is 0 for files?!
2728             "OK",
2729             HTTP::Headers->new(),
2730             );
2731 0           $response->request( $request );
2732              
2733             } elsif( $res = $events{ "MechanizeChrome.download" } ) {
2734             #warn "MechanizeChrome.download";
2735             $response = HTTP::Response->new(
2736             $res->{params}->{response}->{status} || 200, # is 0 for files?!
2737             $res->{params}->{response}->{statusText},
2738 0   0       HTTP::Headers->new( %{ $res->{params}->{response}->{headers} }),
  0            
2739             )
2740              
2741             } else {
2742 0           require Data::Dumper;
2743 0           warn Data::Dumper::Dumper( $events );
2744 0           die join " ", "Chrome behaviour problem: Didn't see a",
2745             "'Network.responseReceived' event for frameId $frameId,",
2746             "requestId $requestId, cannot synthesize response.",
2747             "I saw " . join ",", sort keys %events;
2748             };
2749 0           $response
2750             }
2751              
2752             =head2 C<< $mech->post( $url, %options ) >>
2753              
2754             B<not implemented>
2755              
2756             $mech->post( 'http://example.com',
2757             params => { param => "Hello World" },
2758             headers => {
2759             "Content-Type" => 'application/x-www-form-urlencoded',
2760             },
2761             charset => 'utf-8',
2762             );
2763              
2764             Sends a POST request to C<$url>.
2765              
2766             A C<Content-Length> header will be automatically calculated if
2767             it is not given.
2768              
2769             The following options are recognized:
2770              
2771             =over 4
2772              
2773             =item *
2774              
2775             C<headers> - a hash of HTTP headers to send. If not given,
2776             the content type will be generated automatically.
2777              
2778             =item *
2779              
2780             C<data> - the raw data to send, if you've encoded it already.
2781              
2782             =back
2783              
2784             =cut
2785              
2786             sub post {
2787 0     0 1   my ($self, $url, %options) = @_;
2788             #my $b = $self->tab->{linkedBrowser};
2789 0           $self->invalidate_cached_values;
2790              
2791             #my $flags = 0;
2792             #if ($options{no_cache}) {
2793             # $flags = $self->repl->constant('nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE');
2794             #};
2795              
2796             # If we don't have data, encode the parameters:
2797 0 0         if( !$options{ data }) {
2798 0           my $req= HTTP::Request::Common::POST( $url, $options{params} );
2799             #warn $req->content;
2800 0           carp "Faking content from parameters is not yet supported.";
2801             #$options{ data } = $req->content;
2802             };
2803              
2804             #$options{ charset } ||= 'utf-8';
2805             #$options{ headers } ||= {};
2806             #$options{ headers }->{"Content-Type"} ||= "application/x-www-form-urlencoded";
2807             #if( $options{ charset }) {
2808             # $options{ headers }->{"Content-Type"} .= "; charset=$options{ charset }";
2809             #};
2810              
2811             # Javascript POST implementation taken from
2812             # http://stackoverflow.com/questions/133925/javascript-post-request-like-a-form-submit
2813 0           $self->eval(<<'JS', $url, $options{ params }, 'POST');
2814             function (path, params, method) {
2815             method = method || "post"; // Set method to post by default if not specified.
2816              
2817             // The rest of this code assumes you are not using a library.
2818             // It can be made less wordy if you use one.
2819             var form = document.createElement("form");
2820             form.setAttribute("method", method);
2821             form.setAttribute("action", path);
2822              
2823             for(var key in params) {
2824             if(params.hasOwnProperty(key)) {
2825             var hiddenField = document.createElement("input");
2826             hiddenField.setAttribute("type", "hidden");
2827             hiddenField.setAttribute("name", key);
2828             hiddenField.setAttribute("value", params[key]);
2829              
2830             form.appendChild(hiddenField);
2831             }
2832             }
2833              
2834             document.body.appendChild(form);
2835             form.submit();
2836             }
2837             JS
2838             # Now, how to trick Selenium into fetching the response?
2839             }
2840              
2841             =head2 C<< $mech->reload( %options ) >>
2842              
2843             $mech->reload( ignoreCache => 1 )
2844              
2845             Acts like the reload button in a browser: repeats the current request.
2846             The history (as per the "back" method) is not altered.
2847              
2848             Returns the HTTP::Response object from the reload, or undef if there's no
2849             current request.
2850              
2851             =cut
2852              
2853 0     0 1   sub reload( $self, %options ) {
  0            
  0            
  0            
2854 0 0         if( exists $options{ ignoreCache } ) {
2855 0 0         $options{ ignoreCache } = $options{ ignoreCache } ? JSON::true : JSON::false;
2856             };
2857             $self->_mightNavigate( sub {
2858 0     0     $self->target->send_message('Page.reload', %options )
2859 0           }, navigates => 1, %options)
2860             ->get;
2861             }
2862              
2863             =head2 C<< $mech->set_download_directory( $dir ) >>
2864              
2865             my $downloads = tempdir();
2866             $mech->set_download_directory( $downloads );
2867              
2868             Enables automatic file downloads and sets the directory where the files
2869             will be downloaded to. Setting this to undef will disable downloads again.
2870              
2871             The directory in C<$dir> must be an absolute path, since Chrome does not know
2872             about the current directory of your Perl script.
2873              
2874             =cut
2875              
2876 0     0 0   sub set_download_directory_future( $self, $dir="" ) {
  0            
  0            
  0            
2877 0           $self->{download_directory} = $dir;
2878 0           my $res;
2879 0 0         if( "" eq $dir ) {
2880 0           $res = $self->target->send_message('Page.setDownloadBehavior',
2881             behavior => 'deny',
2882             )
2883             } else {
2884 0           $res = $self->target->send_message('Page.setDownloadBehavior',
2885             behavior => 'allow',
2886             downloadPath => $dir
2887             )
2888             };
2889 0           $res
2890             };
2891              
2892 0     0 1   sub set_download_directory( $self, $dir="" ) {
  0            
  0            
  0            
2893 0           $self->set_download_directory_future($dir)->get
2894             };
2895              
2896             =head2 C<< $mech->cookie_jar >>
2897              
2898             my $cookies = $mech->cookie_jar
2899              
2900             Returns all the Chrome cookies in a L<HTTP::Cookies::ChromeDevTools> instance.
2901             Setting a cookie in there will also set the cookie in Chrome. Note that
2902             the C<< ->cookie_jar >> does not automatically refresh when a new page is
2903             loaded. To manually refresh the state of the cookie jar, use:
2904              
2905             $mech->get('https://example.com/some_page');
2906             $mech->cookie_jar->load;
2907              
2908             =cut
2909              
2910 0     0 1   sub cookie_jar( $self ) {
  0            
  0            
2911 0   0       $self->{cookie_jar} ||= do {
2912 0           my $c = HTTP::Cookies::ChromeDevTools->new( driver => $self->driver );
2913 0           $c->load;
2914 0           $c
2915             };
2916             };
2917              
2918             =head2 C<< $mech->add_header( $name => $value, ... ) >>
2919              
2920             $mech->add_header(
2921             'X-WWW-Mechanize-Chrome' => "I'm using it",
2922             Encoding => 'text/klingon',
2923             );
2924              
2925             This method sets up custom headers that will be sent with B<every> HTTP(S)
2926             request that Chrome makes.
2927              
2928             Note that currently, we only support one value per header.
2929              
2930             Chrome since version 63+ does not allow setting and sending the C<Referer>
2931             header anymore. The bug report is
2932             at L<https://bugs.chromium.org/p/chromium/issues/detail?id=849972>.
2933              
2934             =cut
2935              
2936 0     0     sub _set_extra_headers_future( $self, %headers ) {
  0            
  0            
  0            
2937 0           $self->log('debug',"Setting additional headers", \%headers);
2938             # force-stringify all header values
2939 0           for (values %headers) { $_ = "$_" };
  0            
2940 0           $self->target->send_message('Network.setExtraHTTPHeaders',
2941             headers => \%headers
2942             );
2943             };
2944              
2945 0     0     sub _set_extra_headers( $self, %headers ) {
  0            
  0            
  0            
2946 0           $self->_set_extra_headers_future(
2947             %headers
2948             )->get;
2949             };
2950              
2951 0     0 1   sub add_header( $self, %headers ) {
  0            
  0            
  0            
2952             $self->{ extra_headers } = {
2953 0           %{ $self->{ extra_headers } },
  0            
2954             %headers,
2955             };
2956 0           $self->_set_extra_headers( %{ $self->{ extra_headers } } );
  0            
2957             };
2958              
2959             =head2 C<< $mech->delete_header( $name , $name2... ) >>
2960              
2961             $mech->delete_header( 'User-Agent' );
2962              
2963             Removes HTTP headers from the agent's list of special headers. Note
2964             that Chrome may still send a header with its default value.
2965              
2966             =cut
2967              
2968 0     0 1   sub delete_header( $self, @headers ) {
  0            
  0            
  0            
2969 0           delete @{ $self->{ extra_headers } }{ @headers };
  0            
2970 0           $self->_set_extra_headers( %{ $self->{ extra_headers } } );
  0            
2971             };
2972              
2973             =head2 C<< $mech->reset_headers >>
2974              
2975             $mech->reset_headers();
2976              
2977             Removes all custom headers and makes Chrome send its defaults again.
2978              
2979             =cut
2980              
2981 0     0 1   sub reset_headers( $self ) {
  0            
  0            
2982 0           $self->{ extra_headers } = {};
2983 0           $self->_set_extra_headers();
2984             };
2985              
2986             =head2 C<< $mech->block_urls() >>
2987              
2988             $mech->block_urls( '//facebook.com/js/conversions/tracking.js' );
2989              
2990             Sets the list of blocked URLs. These URLs will not be retrieved by Chrome
2991             when loading a page. This is useful to eliminate tracking images or to test
2992             resilience in face of bad network conditions.
2993              
2994             =cut
2995              
2996 0     0 1   sub block_urls( $self, @urls ) {
  0            
  0            
  0            
2997 0           $self->target->send_message( 'Network.setBlockedURLs',
2998             urls => \@urls
2999             )->get;
3000             }
3001              
3002             =head2 C<< $mech->res() >> / C<< $mech->response(%options) >>
3003              
3004             my $response = $mech->response(headers => 0);
3005              
3006             Returns the current response as a L<HTTP::Response> object.
3007              
3008             =cut
3009              
3010 0     0 1   sub response( $self ) {
  0            
  0            
3011             $self->{response}
3012 0           };
3013              
3014             {
3015 68     68   694 no warnings 'once';
  68         203  
  68         125824  
3016             *res = \&response;
3017             }
3018              
3019             # Call croak or log it, depending on the C< autodie > setting
3020             sub signal_condition {
3021 0     0 0   my ($self,$msg) = @_;
3022 0 0         if ($self->{autodie}) {
3023 0           croak $msg
3024             } else {
3025 0           $self->log( 'warn', $msg );
3026             }
3027             };
3028              
3029             # Call croak on the C< autodie > setting if we have a non-200 status
3030             sub signal_http_status {
3031 0     0 0   my ($self) = @_;
3032 0 0         if ($self->{autodie}) {
3033 0 0 0       if ($self->status and $self->status !~ /^2/ and $self->status != 0) {
      0        
3034             # there was an error
3035 0   0       croak ($self->response()->message || sprintf "Got status code %d", $self->status );
3036             };
3037             } else {
3038             # silent
3039             }
3040             };
3041              
3042             =head2 C<< $mech->success() >>
3043              
3044             $mech->get('https://google.com');
3045             print "Yay"
3046             if $mech->success();
3047              
3048             Returns a boolean telling whether the last request was successful.
3049             If there hasn't been an operation yet, returns false.
3050              
3051             This is a convenience function that wraps C<< $mech->res->is_success >>.
3052              
3053             =cut
3054              
3055             sub success {
3056 0     0 1   my $res = $_[0]->response();
3057 0 0         $res and $res->is_success
3058             }
3059              
3060             =head2 C<< $mech->status() >>
3061              
3062             $mech->get('https://google.com');
3063             print $mech->status();
3064             # 200
3065              
3066             Returns the HTTP status code of the response.
3067             This is a 3-digit number like 200 for OK, 404 for not found, and so on.
3068              
3069             =cut
3070              
3071             sub status {
3072 0     0 1   my ($self) = @_;
3073 0           return $self->response()->code
3074             };
3075              
3076             =head2 C<< $mech->back() >>
3077              
3078             $mech->back();
3079              
3080             Goes one page back in the page history.
3081              
3082             Returns the (new) response.
3083              
3084             =cut
3085              
3086 0     0 1   sub back( $self, %options ) {
  0            
  0            
  0            
3087             $self->_mightNavigate( sub {
3088 0           $self->target->send_message('Page.getNavigationHistory')->then(sub($history) {
3089 0           my $entry = $history->{entries}->[ $history->{currentIndex}-1 ];
3090             $self->target->send_message('Page.navigateToHistoryEntry', entryId => $entry->{id})
3091 0     0     });
  0            
3092 0           }, navigates => 1, %options)
3093             ->get;
3094             };
3095              
3096             =head2 C<< $mech->forward() >>
3097              
3098             $mech->forward();
3099              
3100             Goes one page forward in the page history.
3101              
3102             Returns the (new) response.
3103              
3104             =cut
3105              
3106 0     0 1   sub forward( $self, %options ) {
  0            
  0            
  0            
3107             $self->_mightNavigate( sub {
3108 0           $self->target->send_message('Page.getNavigationHistory')->then(sub($history) {
3109 0           my $entry = $history->{entries}->[ $history->{currentIndex}+1 ];
3110             $self->target->send_message('Page.navigateToHistoryEntry', entryId => $entry->{id})
3111 0     0     });
  0            
3112 0           }, navigates => 1, %options)
3113             ->get;
3114             }
3115              
3116             =head2 C<< $mech->stop() >>
3117              
3118             $mech->stop();
3119              
3120             Stops all loading in Chrome, as if you pressed C<ESC>.
3121              
3122             This function is mostly of use in callbacks or in a timer callback from your
3123             event loop.
3124              
3125             =cut
3126              
3127 0     0 1   sub stop( $self ) {
  0            
  0            
3128 0           $self->target->send_message('Page.stopLoading')->get;
3129             }
3130              
3131             =head2 C<< $mech->uri() >>
3132              
3133             =head2 C<< $mech->uri_future() >>
3134              
3135             print "We are at " . $mech->uri;
3136             print "We are at " . $mech->uri_future->get;
3137              
3138             Returns the current document URI.
3139              
3140             =cut
3141              
3142 0     0 1   sub uri_future( $self ) {
  0            
  0            
3143 0     0     $self->_cached_document->then(sub ($d) {
  0            
  0            
3144 0           return Future->done( URI->new( $d->{root}->{documentURL} ))
3145 0           });
3146             }
3147              
3148 0     0 1   sub uri( $self ) {
  0            
  0            
3149 0           $self->uri_future->get
3150             }
3151              
3152              
3153             =head2 C<< $mech->infinite_scroll( [$wait_time_in_seconds] ) >>
3154              
3155             $new_content_found = $mech->infinite_scroll(3);
3156              
3157             Loads content into pages that have "infinite scroll" capabilities by scrolling
3158             to the bottom of the web page and waiting up to the number of seconds, as set by
3159             the optional C<$wait_time_in_seconds> argument, for the browser to load more
3160             content. The default is to wait up to 20 seconds. For reasonbly fast sites,
3161             the wait time can be set much lower.
3162              
3163             The method returns a boolean C<true> if new content is loaded, C<false>
3164             otherwise. You can scroll to the end (if there is one) of an infinitely
3165             scrolling page like so:
3166              
3167             while( $mech->infinite_scroll ) {
3168             # Tests for exiting the loop earlier
3169             last if $count++ >= 10;
3170             }
3171              
3172             =cut
3173              
3174             sub infinite_scroll {
3175 0     0 1   my $self = shift;
3176 0   0       my $wait_time = shift || 20;
3177              
3178 0           my $current_height = $self->_get_body_height;
3179 0           $self->log('debug', "Current page body height: $current_height");
3180              
3181 0           $self->_scroll_to_bottom;
3182              
3183 0           my $new_height = $self->_get_body_height;
3184 0           $self->log('debug', "New page body height: $new_height");
3185              
3186 0           my $start_time = time();
3187 0           while (!($new_height > $current_height)) {
3188              
3189             # wait for new elements to load until $wait_time is reached
3190 0 0         if (time() - $start_time > $wait_time) {
3191 0           return 0;
3192             }
3193              
3194             # wait 1/10th sec for new elements to load
3195 0           $self->sleep(0.1);
3196 0           $new_height = $self->_get_body_height;
3197             }
3198 0           return 1;
3199             }
3200              
3201             sub _get_body_height {
3202 0     0     my $self = shift;
3203              
3204 0           my ($height) = $self->eval( 'document.body.scrollHeight' );
3205 0           return $height;
3206             }
3207              
3208             sub _scroll_to_bottom {
3209 0     0     my $self = shift;
3210              
3211             # scroll to bottom and wait for some content to load
3212 0           $self->eval( 'window.scroll(0,document.body.scrollHeight + 200)' );
3213 0           $self->sleep(0.1);
3214             }
3215              
3216             =head1 CONTENT METHODS
3217              
3218             =head2 C<< $mech->document_future() >>
3219              
3220             =head2 C<< $mech->document() >>
3221              
3222             print $self->document->{nodeId};
3223              
3224             Returns the C<document> node.
3225              
3226             This is WWW::Mechanize::Chrome specific.
3227              
3228             =cut
3229              
3230 0     0     sub _cached_document($self) {
  0            
  0            
3231 0 0         if( $self->{_document}) {
3232             #warn "Cached document";
3233             return Future->done( $self->{_document} )
3234              
3235 0           } else {
3236             #warn "Requesting fresh document";
3237 0           weaken( my $s = $self );
3238 0     0     return $self->document_future->then(sub ($d) {
  0            
  0            
3239             #warn "Have fresh document";
3240 0           $s->{_document} = $d;
3241             Future->done( $s->{_document} )
3242 0           });
  0            
3243             }
3244             }
3245              
3246             sub _clear_cached_document {
3247 0     0     delete $_[0]->{_document};
3248             };
3249              
3250             # Move to DOMSnapshot.captureSnapshot / DOMSnapshot.DocumentSnapshot instead
3251 0     0 1   sub document_future( $self ) {
  0            
  0            
3252 0           return $self->target->send_message('DOM.getDocument', depth => -1, pierce => JSON::false );
3253             }
3254              
3255 0     0 1   sub document( $self ) {
  0            
  0            
3256 0           $self->_cached_document->get
3257             }
3258              
3259 0     0 0   sub decoded_content($self) {
  0            
  0            
3260 0           my $res;
3261 0   0       my $ct = $self->ct || 'text/html';
3262 0 0         if( $ct eq 'text/html' ) {
3263 0     0     $res = $self->_cached_document->then(sub( $root ) {
  0            
  0            
3264             # Join _all_ child nodes together to also fetch DOCTYPE nodes
3265             # and the stuff that comes after them
3266              
3267             my @content = map {
3268 0           my $nodeId = $_->{nodeId};
3269 0           $self->log('trace', "Fetching HTML for node " . $nodeId );
3270 0           $self->target->send_message('DOM.getOuterHTML', nodeId => 0+$nodeId )
3271 0           } @{ $root->{root}->{children} };
  0            
3272              
3273             return Future->wait_all( @content )
3274 0           ->then( sub( @outerHTML_f ) {
3275 0           Future->done( join "", map { $_->get->{outerHTML} } @outerHTML_f );
  0            
3276 0           });
3277 0           });
3278             } else {
3279             # Return the raw body
3280             #use Data::Dumper;
3281             #warn Dumper $self->response;
3282             #warn $self->response->content;
3283              
3284             # The content is already decoded (?!)
3285             # I'm not sure how well this plays with encodings, and
3286             # binary content
3287 0           $res = Future->done($self->response->content);
3288             };
3289 0           return $res->get
3290             };
3291              
3292             =head2 C<< $mech->content( %options ) >>
3293              
3294             print $mech->content;
3295             print $mech->content( format => 'html' ); # default
3296             print $mech->content( format => 'text' ); # identical to ->text
3297             print $mech->content( format => 'mhtml' ); # identical to ->captureSnapshot
3298              
3299             This always returns the content as a Unicode string. It tries
3300             to decode the raw content according to its input encoding.
3301             This currently only works for HTML pages, not for images etc.
3302              
3303             Recognized options:
3304              
3305             =over 4
3306              
3307             =item *
3308              
3309             C<format> - the stuff to return
3310              
3311             The allowed values are C<html> and C<text>. The default is C<html>.
3312              
3313             =back
3314              
3315             =cut
3316              
3317 0     0 1   sub content( $self, %options ) {
  0            
  0            
  0            
3318 0   0       $options{ format } ||= 'html';
3319 0           my $format = delete $options{ format };
3320              
3321 0           my $content;
3322 0 0         if( 'html' eq $format ) {
    0          
    0          
3323 0           $content= $self->decoded_content()
3324             } elsif ( $format eq 'text' ) {
3325 0           $content= $self->text;
3326             } elsif ( $format eq 'mhtml' ) {
3327 0           $content= $self->captureSnapshot()->{data};
3328             } else {
3329 0           die qq{Unknown "format" parameter "$format"};
3330             };
3331             };
3332              
3333             =head2 C<< $mech->text() >>
3334              
3335             print $mech->text();
3336              
3337             Returns the text of the current HTML content. If the content isn't
3338             HTML, $mech will die.
3339              
3340             =cut
3341              
3342             sub text {
3343 0     0 1   my $self = shift;
3344              
3345             # Waugh - this is highly inefficient but conveniently short to write
3346             # Maybe this should skip SCRIPT nodes...
3347 0           join '', map { $_->get_attribute('innerText', live => 1) } $self->xpath('//body', single => 1 );
  0            
3348             }
3349              
3350             =head2 C<< $mech->captureSnapshot_future() >>
3351              
3352             =head2 C<< $mech->captureSnapshot() >>
3353              
3354             print $mech->captureSnapshot( format => 'mhtml' )->{data};
3355              
3356             Returns the current page as MHTML.
3357              
3358             This is WWW::Mechanize::Chrome specific.
3359              
3360             =cut
3361              
3362 0     0 1   sub captureSnapshot_future( $self, %options ) {
  0            
  0            
  0            
3363 0           $self->target->send_message( 'Page.captureSnapshot', %options )
3364             }
3365              
3366 0     0 1   sub captureSnapshot( $self, %options ) {
  0            
  0            
  0            
3367 0           $self->captureSnapshot_future(%options)->get
3368             }
3369              
3370             =head2 C<< $mech->content_encoding() >>
3371              
3372             print "The content is encoded as ", $mech->content_encoding;
3373              
3374             Returns the encoding that the content is in. This can be used
3375             to convert the content from UTF-8 back to its native encoding.
3376              
3377             =cut
3378              
3379             sub content_encoding {
3380 0     0 1   my ($self) = @_;
3381             # Let's trust the <meta http-equiv first, and the header second:
3382             # Also, a pox on Chrome for not having lower-case or upper-case
3383 0 0         if(( my $meta )= $self->xpath( q{//meta[translate(@http-equiv,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')="content-type"]}, first => 1 )) {
3384 0           (my $ct= $meta->{attributes}->{'content'}) =~ s/^.*;\s*charset=\s*//i;
3385 0 0         return $ct
3386             if( $ct );
3387             };
3388 0           $self->response->header('Content-Type');
3389             };
3390              
3391             =head2 C<< $mech->update_html( $html ) >>
3392              
3393             $mech->update_html($html);
3394              
3395             Writes C<$html> into the current document. This is mostly
3396             implemented as a convenience method for L<HTML::Display::MozRepl>.
3397              
3398             The value passed in as C<$html> will be stringified.
3399              
3400             =cut
3401              
3402 0     0 1   sub update_html( $self, $content ) {
  0            
  0            
  0            
3403 0           my $doc = $self->_cached_document;
3404 0     0     $doc->then(sub( $root ) {
  0            
  0            
3405             # Find "HTML" child node:
3406 0           my $nodeId = $root->{root}->{children}->[0]->{nodeId};
3407 0           my $id;
3408 0 0         if( ! $nodeId ) {
3409 68     68   653 use Data::Dumper;
  68         203  
  68         11482  
3410 0           warn Dumper $root;
3411 0           warn "Need / fetching nodeId from backendNodeId";
3412              
3413 0           my @parentNodes; # we only expect one ...
3414 0           my $setChildNodes = $self->add_listener('DOM.setChildNodes', sub( $ev ) {
3415             #use Data::Dumper; warn "setChildNodes: "; warn Dumper $ev;
3416 0           push @parentNodes, @{ $ev->{params}->{nodes} };
  0            
3417 0           });
3418              
3419             $id = $self->target->send_message('DOM.resolveNode', backendNodeId => $root->{root}->{children}->[0]->{backendNodeId} )
3420 0           ->then( sub ( $nodeInfo ) {
3421 68     68   600 use Data::Dumper;
  68         232  
  68         7759  
3422 0           warn Dumper $nodeInfo;
3423             $self->target->send_message('DOM.requestNode', objectId => $nodeInfo->{object}->{objectId})
3424             #return Future->done( $nodeInfo->{node}->{nodeId} )
3425 0           })->then(sub ( $node ) {
  0            
3426              
3427             # Implicitly, @parentNodes has been filled ...
3428              
3429 68     68   560 use Data::Dumper;
  68         188  
  68         30365  
3430 0           warn Dumper $node;
3431             return Future->done( $node->{nodeId} )
3432             #return Future->done( $childNodes[0]->{nodeId} )
3433 0           });
  0            
3434              
3435             } else {
3436 0           $id = $self->target->future->done( $nodeId );
3437             };
3438              
3439             $id->then( sub {
3440 0           $self->log('trace', "Setting HTML for node " . $nodeId );
3441             $self->target->send_message('DOM.setOuterHTML', nodeId => 0+$nodeId, outerHTML => "$content" )
3442             ->then(sub {;
3443 0           $self->invalidate_cached_values;
3444 0           Future->done()
3445             })
3446              
3447             # Also, we need to wait for a DOM.documentUpdated here before querying
3448             # again ... do we?!
3449 0           });
  0            
3450 0           })->get;
3451             };
3452              
3453             =head2 C<< $mech->base() >>
3454              
3455             print $mech->base;
3456              
3457             Returns the URL base for the current page.
3458              
3459             The base is either specified through a C<base>
3460             tag or is the current URL.
3461              
3462             This method is specific to WWW::Mechanize::Chrome.
3463              
3464             =cut
3465              
3466             sub base {
3467 0     0 1   my ($self) = @_;
3468 0           (my $base) = $self->selector('base');
3469 0 0         $base = $base->get_attribute('href', live => 1)
3470             if $base;
3471 0   0       $base ||= $self->uri;
3472             };
3473              
3474             =head2 C<< $mech->content_type() >>
3475              
3476             =head2 C<< $mech->ct() >>
3477              
3478             print $mech->content_type;
3479              
3480             Returns the content type of the currently loaded document
3481              
3482             =cut
3483              
3484             sub content_type {
3485 0     0 1   my ($self) = @_;
3486             # Let's trust the <meta http-equiv first, and the header second:
3487             # Also, a pox on Chrome for not having lower-case or upper-case
3488 0           my $ct;
3489 0 0         if(my( $meta )= $self->xpath( q{//meta[translate(@http-equiv,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')="content-type"]}, first => 1 )) {
3490 0           $ct= $meta->{attributes}->{'content'};
3491             };
3492 0 0 0       if(!$ct and my $r= $self->response ) {
3493              
3494 0           my $h= $r->headers;
3495 0           $ct= $h->header('Content-Type');
3496             };
3497 0 0         $ct =~ s/;.*$// if defined $ct;
3498 0           $ct
3499             };
3500              
3501             {
3502 68     68   574 no warnings 'once';
  68         192  
  68         975578  
3503             *ct = \&content_type;
3504             }
3505              
3506             =head2 C<< $mech->is_html() >>
3507              
3508             print $mech->is_html();
3509              
3510             Returns true/false on whether our content is HTML, according to the
3511             HTTP headers.
3512              
3513             =cut
3514              
3515             sub is_html {
3516 0     0 1   my $self = shift;
3517 0   0       return defined $self->ct && ($self->ct eq 'text/html');
3518             }
3519              
3520             =head2 C<< $mech->title() >>
3521              
3522             print "We are on page " . $mech->title;
3523              
3524             Returns the current document title.
3525              
3526             =cut
3527              
3528 0     0 1   sub title( $self ) {
  0            
  0            
3529             $self->target->info->{title}
3530 0           };
3531              
3532             =head1 EXTRACTION METHODS
3533              
3534             =head2 C<< $mech->links() >>
3535              
3536             print $_->text . " -> " . $_->url . "\n"
3537             for $mech->links;
3538              
3539             Returns all links in the document as L<WWW::Mechanize::Link> objects.
3540              
3541             Currently accepts no parameters. See C<< ->xpath >>
3542             or C<< ->selector >> when you want more control.
3543              
3544             =cut
3545              
3546             our %link_spec = (
3547             a => { url => 'href', },
3548             area => { url => 'href', },
3549             frame => { url => 'src', },
3550             iframe => { url => 'src', },
3551             link => { url => 'href', },
3552             meta => { url => 'content', xpath => (join '',
3553             q{translate(@http-equiv,'ABCDEFGHIJKLMNOPQRSTUVWXYZ',},
3554             q{'abcdefghijklmnopqrstuvwxyz')="refresh"}), },
3555             );
3556             # taken from WWW::Mechanize. This should possibly just be reused there
3557             sub make_link {
3558 0     0 0   my ($self,$node,$base) = @_;
3559              
3560 0           my $tag = lc $node->get_tag_name;
3561 0           my $url;
3562 0 0         if ($tag) {
3563 0 0         if( ! exists $link_spec{ $tag }) {
3564 0           carp "Unknown link-spec tag '$tag'";
3565 0           $url= '';
3566             } else {
3567 0           $url = $node->get_attribute( $link_spec{ $tag }->{url}, live => 1 );
3568             };
3569             };
3570              
3571 0 0         if ($tag eq 'meta') {
3572 0           my $content = $url;
3573 0 0         if ( $content =~ /^\d+\s*;\s*url\s*=\s*(\S+)/i ) {
3574 0           $url = $1;
3575 0 0         $url =~ s/^"(.+)"$/$1/ or $url =~ s/^'(.+)'$/$1/;
3576             }
3577             else {
3578 0           undef $url;
3579             }
3580             };
3581              
3582 0 0         if (defined $url) {
3583             #my $text => $node->get_attribute('text'),
3584 0           my $text = $node->get_text;
3585 0           $text =~ s!\A\s+!!s;
3586 0           $text =~ s!\s+\z!!s;
3587 0           my $res = WWW::Mechanize::Link->new({
3588             tag => $tag,
3589             name => $node->get_attribute('name', live => 1),
3590             base => $base,
3591             url => $url,
3592             text => $text,
3593             attrs => {},
3594             });
3595 0           return $res
3596             } else {
3597             ()
3598 0           };
3599             }
3600              
3601             sub links {
3602 0     0 1   my ($self) = @_;
3603 0           my @links = $self->selector( join ",", sort keys %link_spec);
3604 0           my $base = $self->base;
3605             return map {
3606 0           $self->make_link($_,$base)
  0            
3607             } @links;
3608             };
3609              
3610             =head2 C<< $mech->selector( $css_selector, %options ) >>
3611              
3612             my @text = $mech->selector('p.content');
3613              
3614             Returns all nodes matching the given CSS selector. If
3615             C<$css_selector> is an array reference, it returns
3616             all nodes matched by any of the CSS selectors in the array.
3617              
3618             This takes the same options that C<< ->xpath >> does.
3619              
3620             This method is implemented via L<WWW::Mechanize::Plugin::Selector>.
3621              
3622             =cut
3623              
3624             sub selector {
3625 0     0 1   my ($self,$query,%options) = @_;
3626 0   0       $options{ user_info } ||= "CSS selector '$query'";
3627 0 0 0       if ('ARRAY' ne (ref $query || '')) {
3628 0           $query = [$query];
3629             };
3630 0 0         my $root = $options{ node } ? './' : '';
3631 0           my @q = map { selector_to_xpath($_, root => $root) } @$query;
  0            
3632 0           $self->xpath(\@q, %options);
3633             };
3634              
3635             =head2 C<< $mech->find_link_dom( %options ) >>
3636              
3637             print $_->{innerHTML} . "\n"
3638             for $mech->find_link_dom( text_contains => 'CPAN' );
3639              
3640             A method to find links, like L<WWW::Mechanize>'s
3641             C<< ->find_links >> method. This method returns DOM objects from
3642             Chrome instead of WWW::Mechanize::Link objects.
3643              
3644             Note that Chrome
3645             might have reordered the links or frame links in the document
3646             so the absolute numbers passed via C<n>
3647             might not be the same between
3648             L<WWW::Mechanize> and L<WWW::Mechanize::Chrome>.
3649              
3650             The supported options are:
3651              
3652             =over 4
3653              
3654             =item *
3655              
3656             C<< text >> and C<< text_contains >> and C<< text_regex >>
3657              
3658             Match the text of the link as a complete string, substring or regular expression.
3659              
3660             Matching as a complete string or substring is a bit faster, as it is
3661             done in the XPath engine of Chrome.
3662              
3663             =item *
3664              
3665             C<< id >> and C<< id_contains >> and C<< id_regex >>
3666              
3667             Matches the C<id> attribute of the link completely or as part
3668              
3669             =item *
3670              
3671             C<< name >> and C<< name_contains >> and C<< name_regex >>
3672              
3673             Matches the C<name> attribute of the link
3674              
3675             =item *
3676              
3677             C<< url >> and C<< url_regex >>
3678              
3679             Matches the URL attribute of the link (C<href>, C<src> or C<content>).
3680              
3681             =item *
3682              
3683             C<< class >> - the C<class> attribute of the link
3684              
3685             =item *
3686              
3687             C<< n >> - the (1-based) index. Defaults to returning the first link.
3688              
3689             =item *
3690              
3691             C<< single >> - If true, ensure that only one element is found. Otherwise croak
3692             or carp, depending on the C<autodie> parameter.
3693              
3694             =item *
3695              
3696             C<< one >> - If true, ensure that at least one element is found. Otherwise croak
3697             or carp, depending on the C<autodie> parameter.
3698              
3699             The method C<croak>s if no link is found. If the C<single> option is true,
3700             it also C<croak>s when more than one link is found.
3701              
3702             =back
3703              
3704             =cut
3705              
3706             our %xpath_quote = (
3707             '"' => '\"',
3708             #"'" => "\\'",
3709             #'[' => '&#91;',
3710             #']' => '&#93;',
3711             #'[' => '[\[]',
3712             #'[' => '\[',
3713             #']' => '[\]]',
3714             );
3715              
3716             sub quote_xpath {
3717 0     0 0   local $_ = $_[0];
3718 0 0         s/(['"\[\]])/$xpath_quote{$1} || $1/ge;
  0            
3719 0           $_
3720             };
3721              
3722             # Copied from WWW::Mechanize 1.97
3723             # Used by find_links to check for matches
3724             # The logic is such that ALL param criteria that are given must match
3725 0     0     sub _match_any_link_params( $self, $link, $p ) {
  0            
  0            
  0            
  0            
3726             # No conditions, anything matches
3727 0 0         return 1 unless keys %$p;
3728              
3729 0 0 0       return if defined $p->{url} && !($link->url eq $p->{url} );
3730 0 0 0       return if defined $p->{url_regex} && !($link->url =~ $p->{url_regex} );
3731 0 0 0       return if defined $p->{url_abs} && !($link->url_abs eq $p->{url_abs} );
3732 0 0 0       return if defined $p->{url_abs_regex} && !($link->url_abs =~ $p->{url_abs_regex} );
3733 0 0 0       return if defined $p->{text} && !(defined($link->text) && $link->text eq $p->{text} );
      0        
3734 0 0 0       return if defined $p->{text_regex} && !(defined($link->text) && $link->text =~ $p->{text_regex} );
      0        
3735 0 0 0       return if defined $p->{name} && !(defined($link->name) && $link->name eq $p->{name} );
      0        
3736 0 0 0       return if defined $p->{name_regex} && !(defined($link->name) && $link->name =~ $p->{name_regex} );
      0        
3737 0 0 0       return if defined $p->{tag} && !($link->tag && $link->tag eq $p->{tag} );
      0        
3738 0 0 0       return if defined $p->{tag_regex} && !($link->tag && $link->tag =~ $p->{tag_regex} );
      0        
3739              
3740 0 0 0       return if defined $p->{id} && !($link->attrs->{id} && $link->attrs->{id} eq $p->{id} );
      0        
3741 0 0 0       return if defined $p->{id_regex} && !($link->attrs->{id} && $link->attrs->{id} =~ $p->{id_regex} );
      0        
3742 0 0 0       return if defined $p->{class} && !($link->attrs->{class} && $link->attrs->{class} eq $p->{class} );
      0        
3743 0 0 0       return if defined $p->{class_regex} && !($link->attrs->{class} && $link->attrs->{class} =~ $p->{class_regex} );
      0        
3744              
3745             # Success: everything that was defined passed.
3746 0           return 1;
3747             }
3748              
3749             sub find_link_dom {
3750 0     0 1   my ($self,%opts) = @_;
3751 0           my %xpath_options;
3752              
3753             # Clean up some legacy stuff
3754 0           delete @opts{ qw(synchronize) };
3755              
3756 0           for (qw(node document frames xpath selector)) {
3757             # Copy over XPath options that were passed in
3758 0 0         if (exists $opts{ $_ }) {
3759 0           $xpath_options{ $_ } = delete $opts{ $_ };
3760             };
3761             };
3762              
3763 0           my $single = delete $opts{ single };
3764 0   0       my $one = delete $opts{ one } || $single;
3765 0 0 0       if ($single and exists $opts{ n }) {
3766 0           croak "It doesn't make sense to use 'single' and 'n' option together"
3767             };
3768 0   0       my $n = (delete $opts{ n } || 1);
3769 0 0         $n--
3770             if ($n ne 'all'); # 1-based indexing
3771 0           my @spec;
3772              
3773             # Decode text and text_contains into XPath
3774 0           for my $lvalue (qw( text id name class )) {
3775 0           my %lefthand = (
3776             text => 'text()',
3777             );
3778 0           my %match_op = (
3779             '' => q{%s="%s"},
3780             'contains' => q{contains(%s,"%s")},
3781             # Ideally we would also handle *_regex here, but Chrome XPath
3782             # does not support fn:matches() :-(
3783             #'regex' => q{matches(%s,"%s","%s")},
3784             );
3785 0   0       my $lhs = $lefthand{ $lvalue } || '@'.$lvalue;
3786 0           for my $op (keys %match_op) {
3787 0           my $v = $match_op{ $op };
3788 0 0         $op = '_'.$op if length($op);
3789 0           my $key = "${lvalue}$op";
3790              
3791 0 0         if (exists $opts{ $key }) {
3792 0           my $p = delete $opts{ $key };
3793 0           push @spec, sprintf $v, $lhs, $p;
3794             };
3795             };
3796             };
3797              
3798 0 0         if (my $p = delete $opts{ url }) {
3799 0           push @spec, sprintf '@href = "%s" or @src="%s"', quote_xpath( $p ), quote_xpath( $p );
3800             }
3801 0           my @tags = (sort keys %link_spec);
3802 0 0         if (my $p = delete $opts{ tag }) {
3803 0           @tags = $p;
3804             };
3805 0 0         if (my $p = delete $opts{ tag_regex }) {
3806 0           @tags = grep /$p/, @tags;
3807             };
3808             my $q = join '|',
3809             map {
3810 0 0         my $xp= exists $link_spec{ $_ } ? $link_spec{$_}->{xpath} : undef;
  0            
3811 0           my @full = map {qq{($_)}} grep {defined} (@spec, $xp);
  0            
  0            
3812 0 0         if (@full) {
3813 0           sprintf "//%s[%s]", $_, join " and ", @full;
3814             } else {
3815 0           sprintf "//%s", $_
3816             };
3817             } (@tags);
3818             #warn $q;
3819              
3820 0           my @res = $self->xpath($q, %xpath_options );
3821              
3822 0 0         if (keys %opts) {
3823             # post-filter the remaining links
3824             # for all the options we don't support with XPath
3825 0           my $base = $self->base;
3826              
3827             @res = grep {
3828 0           $self->_match_any_link_params($self->make_link($_,$base),\%opts);
  0            
3829             } @res;
3830             };
3831              
3832 0 0         if ($one) {
3833 0 0         if (0 == @res) { $self->signal_condition( "No link found matching '$q'" )};
  0            
3834 0 0         if ($single) {
3835 0 0         if (1 < @res) {
3836 0           $self->highlight_node(@res);
3837 0           $self->signal_condition(
3838             sprintf "%d elements found found matching '%s'", scalar @res, $q
3839             );
3840             };
3841             };
3842             };
3843              
3844 0 0         if ($n eq 'all') {
3845             return @res
3846 0           };
3847 0           $res[$n]
3848             }
3849              
3850             =head2 C<< $mech->find_link( %options ) >>
3851              
3852             print $_->text . "\n"
3853             for $mech->find_link( text_contains => 'CPAN' );
3854              
3855             A method quite similar to L<WWW::Mechanize>'s method.
3856             The options are documented in C<< ->find_link_dom >>.
3857              
3858             Returns a L<WWW::Mechanize::Link> object.
3859              
3860             This defaults to not look through child frames.
3861              
3862             =cut
3863              
3864             sub find_link {
3865 0     0 1   my ($self,%opts) = @_;
3866 0           my $base = $self->base;
3867             croak "Option 'all' not available for ->find_link. Did you mean to call ->find_all_links()?"
3868 0 0 0       if 'all' eq ($opts{n} || '');
3869 0 0         if (my $link = $self->find_link_dom(frames => 0, %opts)) {
3870 0           return $self->make_link($link, $base)
3871             } else {
3872             return
3873 0           };
3874             };
3875              
3876             =head2 C<< $mech->find_all_links( %options ) >>
3877              
3878             print $_->text . "\n"
3879             for $mech->find_all_links( text_regex => qr/google/i );
3880              
3881             Finds all links in the document.
3882             The options are documented in C<< ->find_link_dom >>.
3883              
3884             Returns them as list or an array reference, depending
3885             on context.
3886              
3887             This defaults to not look through child frames.
3888              
3889             =cut
3890              
3891             sub find_all_links {
3892 0     0 1   my ($self, %opts) = @_;
3893 0           $opts{ n } = 'all';
3894 0           my $base = $self->base;
3895             my @matches = map {
3896 0           $self->make_link($_, $base);
  0            
3897             } $self->find_all_links_dom( frames => 0, %opts );
3898 0 0         return @matches if wantarray;
3899 0           return \@matches;
3900             };
3901              
3902             =head2 C<< $mech->find_all_links_dom %options >>
3903              
3904             print $_->{innerHTML} . "\n"
3905             for $mech->find_all_links_dom( text_regex => qr/google/i );
3906              
3907             Finds all matching linky DOM nodes in the document.
3908             The options are documented in C<< ->find_link_dom >>.
3909              
3910             Returns them as list or an array reference, depending
3911             on context.
3912              
3913             This defaults to not look through child frames.
3914              
3915             =cut
3916              
3917             sub find_all_links_dom {
3918 0     0 1   my ($self,%opts) = @_;
3919 0           $opts{ n } = 'all';
3920 0           my @matches = $self->find_link_dom( frames => 0, %opts );
3921 0 0         return @matches if wantarray;
3922 0           return \@matches;
3923             };
3924              
3925             =head2 C<< $mech->follow_link( $link ) >>
3926              
3927             =head2 C<< $mech->follow_link( %options ) >>
3928              
3929             $mech->follow_link( xpath => '//a[text() = "Click here!"]' );
3930              
3931             Follows the given link. Takes the same parameters that C<find_link_dom>
3932             uses.
3933              
3934             Note that C<< ->follow_link >> will only try to follow link-like
3935             things like C<A> tags.
3936              
3937             =cut
3938              
3939             sub follow_link {
3940 0     0 1   my ($self,$link,%opts);
3941 0 0         if (@_ == 2) { # assume only a link parameter
3942 0           ($self,$link) = @_;
3943 0           $self->click($link);
3944             } else {
3945 0           ($self,%opts) = @_;
3946 0           _default_limiter( one => \%opts );
3947 0           $link = $self->find_link_dom(%opts);
3948 0           $self->click({ dom => $link, %opts });
3949             }
3950             }
3951              
3952             sub activate_parent_container {
3953 0     0 0   my( $self, $doc )= @_;
3954 0           $self->activate_container( $doc, 1 );
3955             };
3956              
3957             sub activate_container {
3958 0     0 0   my( $self, $doc, $just_parent )= @_;
3959 0           my $driver= $self->driver;
3960              
3961 0 0         if( ! $doc->{__path}) {
3962 0           die "Invalid document without __path encountered. I'm sorry.";
3963             };
3964             # Activate the root window/frame
3965             #warn "Activating root frame:";
3966             #$driver->switch_to_frame();
3967             #warn "Activating root frame done.";
3968              
3969 0           for my $el ( @{ $doc->{__path} }) {
  0            
3970             #warn "Switching frames downwards ($el)";
3971             #warn "Tag: " . $el->get_tag_name;
3972             #warn Dumper $el;
3973 0           warn sprintf "Switching during path to %s %s", $el->get_tag_name, $el->get_attribute('src', live => 1);
3974 0           $driver->switch_to_frame( $el );
3975             };
3976              
3977 0 0         if( ! $just_parent ) {
3978 0           warn sprintf "Activating container %s too", $doc->{id};
3979             # Now, unless it's the root frame, activate the container. The root frame
3980             # already is activated above.
3981 0           warn "Getting tag";
3982 0           my $tag= $doc->get_tag_name;
3983             #my $src= $doc->get_attribute('src');
3984 0 0 0       if( 'html' ne $tag and '' ne $tag) {
3985             #warn sprintf "Switching to final container %s %s", $tag, $src;
3986 0           $driver->switch_to_frame( $doc );
3987             };
3988             #warn sprintf "Switched to final/main container %s %s", $tag, $src;
3989             };
3990             #warn $self->target->get_current_url;
3991             #warn $self->target->get_title;
3992             #my $body= $doc->get_attribute('contentDocument');
3993 0           my $body= $driver->find_element('/*', 'xpath');
3994 0 0         if( $body ) {
3995 0           warn "Now active container: " . $body->get_attribute('innerHTML', live => 1);
3996             #$body= $body->get_attribute('document');
3997             #warn $body->get_attribute('innerHTML');
3998             };
3999             };
4000              
4001             =head2 C<< $mech->xpath( $query, %options ) >>
4002              
4003             my $link = $mech->xpath('//a[id="clickme"]', one => 1);
4004             # croaks if there is no link or more than one link found
4005              
4006             my @para = $mech->xpath('//p');
4007             # Collects all paragraphs
4008              
4009             my @para_text = $mech->xpath('//p/text()', type => $mech->xpathResult('STRING_TYPE'));
4010             # Collects all paragraphs as text
4011              
4012             Runs an XPath query in Chrome against the current document.
4013              
4014             If you need more information about the returned results,
4015             use the C<< ->xpathEx() >> function.
4016              
4017             Note that Chrome sometimes returns a node with node id 0. This node then
4018             cannot be found again using the Chrome API. This is bad luck and results in
4019             a warning.
4020              
4021             The options allow the following keys:
4022              
4023             =over 4
4024              
4025             =item *
4026              
4027             C<< document >> - document in which the query is to be executed. Use this to
4028             search a node within a specific subframe of C<< $mech->document >>.
4029              
4030             =item *
4031              
4032             C<< frames >> - if true, search all documents in all frames and iframes.
4033             This may or may not conflict with C<node>. This will default to the
4034             C<frames> setting of the WWW::Mechanize::Chrome object.
4035              
4036             =item *
4037              
4038             C<< node >> - node relative to which the query is to be executed. Note
4039             that you will have to use a relative XPath expression as well. Use
4040              
4041             .//foo
4042              
4043             instead of
4044              
4045             //foo
4046              
4047             Querying relative to a node only works for restricting to children of the node,
4048             not for anything else. This is because we need to do the ancestor filtering
4049             ourselves instead of having a Chrome API for it.
4050              
4051             =item *
4052              
4053             C<< single >> - If true, ensure that only one element is found. Otherwise croak
4054             or carp, depending on the C<autodie> parameter.
4055              
4056             =item *
4057              
4058             C<< one >> - If true, ensure that at least one element is found. Otherwise croak
4059             or carp, depending on the C<autodie> parameter.
4060              
4061             =item *
4062              
4063             C<< maybe >> - If true, ensure that at most one element is found. Otherwise
4064             croak or carp, depending on the C<autodie> parameter.
4065              
4066             =item *
4067              
4068             C<< all >> - If true, return all elements found. This is the default.
4069             You can use this option if you want to use C<< ->xpath >> in scalar context
4070             to count the number of matched elements, as it will otherwise emit a warning
4071             for each usage in scalar context without any of the above restricting options.
4072              
4073             =item *
4074              
4075             C<< any >> - no error is raised, no matter if an item is found or not.
4076              
4077             =back
4078              
4079             Returns the matched results as L<WWW::Mechanize::Chrome::Node> objects.
4080              
4081             You can pass in a list of queries as an array reference for the first parameter.
4082             The result will then be the list of all elements matching any of the queries.
4083              
4084             This is a method that is not implemented in WWW::Mechanize.
4085              
4086             In the long run, this should go into a general plugin for
4087             L<WWW::Mechanize>.
4088              
4089             =cut
4090              
4091             # This unwraps a tree of child nodes into a flat hash indexed by nodeId
4092 0     0     sub _unwrapChildNodeTree( $self, $nodes, $tree={} ) {
  0            
  0            
  0            
  0            
4093 0           for my $node (@$nodes) {
4094 0           $tree->{ $node->{nodeId} } = $node;
4095 0 0         if( $node->{children}) {
4096 0           $self->_unwrapChildNodeTree( $node->{children}, $tree );
4097             };
4098             }
4099 0           return $tree
4100             }
4101              
4102 0     0     sub _performSearch( $self, %args ) {
  0            
  0            
  0            
4103 0           my $subTreeId = $args{ subTreeId };
4104 0           my $query = $args{ query };
4105 0           weaken( my $s = $self );
4106              
4107 0           my $doc;
4108             # Retry a search up to three times if the page changes in the meantime
4109             my $nodeGeneration;
4110 0   0       $s->{_currentNodeGeneration} //= 0;
4111 0           my $retries = 3;
4112 0           my $last_search;
4113             my $search = repeat {
4114 0     0     $nodeGeneration = $self->{_currentNodeGeneration};
4115             # Lock the document, hoping that no intermittent update messes up our IDs
4116             # Just to make sure we avoid nodeId 0 ?!
4117             # https://github.com/cyrus-and/chrome-remote-interface/issues/165
4118 0           my $wait = $s->_cached_document->then(sub( $r ) {
4119 0           $doc = $r->{root};
4120 0           Future->done
4121 0           });
4122              
4123 0           $wait = $wait->then( sub(@info) {
4124 0           my $res = $s->target->send_message( 'DOM.performSearch', query => $query );
4125 0           return $res
4126 0           });
4127 0           return $wait
4128              
4129 0     0     } while => sub($search) {
  0            
  0            
4130 0   0       my $retry = ($nodeGeneration != $s->{_currentNodeGeneration} and $retries--);
4131              
4132 0 0         if( $retry ) {
4133             # close the previous search attempt
4134 0           my $se = $search->then(sub($results) {
4135 0           my $searchId = $results->{searchId};
4136             #warn "!!! Discarding search";
4137 0           $s->target->send_message( 'DOM.discardSearchResults',
4138             searchId => $searchId,
4139             );
4140 0           });
4141             #warn "Closed search: $se";
4142 0           $se->retain;
4143             }
4144              
4145 0 0         if( $retry ) {
4146 0           $s->log('trace', "Retrying search ($retries attempts left)");
4147             }
4148             $retry
4149 0           };
  0            
4150              
4151 0     0     $search->then(sub($results) {
  0            
  0            
4152 0           $s->log('debug', "XPath query '$query' (". $results->{resultCount} . " node(s))");
4153              
4154 0 0         if( $results->{resultCount} ) {
4155 0           my $searchResults;
4156 0           my $searchId = $results->{searchId};
4157 0           my @childNodes;
4158 0           my $setChildNodes = $self->add_listener('DOM.setChildNodes', sub( $ev ) {
4159             #use Data::Dumper; warn "setChildNodes: "; warn Dumper $ev;
4160 0           push @childNodes, @{ $ev->{params}->{nodes} };
  0            
4161 0           });
4162              
4163 0           my $childNodes;
4164 0 0         if( defined $subTreeId ) {
4165 0           $childNodes =
4166             $self->target->send_message( 'DOM.requestChildNodes',
4167             nodeId => 0+$subTreeId,
4168             depth => -1, # we want/need the whole subtree
4169             )
4170             } else {
4171 0           $childNodes = Future->done;
4172             };
4173             my $search = $self->target->send_message( 'DOM.getSearchResults',
4174             searchId => $results->{searchId},
4175             fromIndex => 0,
4176             toIndex => 0+$results->{resultCount},
4177 0           );
4178             # We can't immediately discard our search results until we find out
4179             # what invalidates node ids.
4180             # So we currently accumulate memory until we disconnect. Oh well.
4181             # And node ids still get invalidated
4182             #)->followed_by( sub( $results ) {
4183             # $searchResults = $results->get;
4184             # $self->target->send_message( 'DOM.discardSearchResults',
4185             # searchId => $searchId,
4186             # );
4187             #}
4188              
4189             Future->wait_all( $childNodes, $search )->then(sub {
4190             # The result of $childNodes is indirect here, by pushing
4191             # the setChildNodes messages onto @childNodes
4192 0           my @discard = $childNodes->get();
4193              
4194 0           return $search;
4195              
4196 0           })->then( sub( $response ) {
4197             # you might get a node with nodeId 0. This one
4198             # can't be retrieved. Bad luck.
4199 0 0         if($response->{nodeIds}->[0] == 0) {
4200             # Maybe we did receive exactly one childnode?!
4201             #if( @childNodes == 1 ) {
4202             # warn "Maybe we can hacky-salvage this?! Forcing nodeId to $childNodes[0]->{nodeId}";
4203             # # Nope - in the bad case, we always get the root node
4204             # # instead of something usable :-/
4205             # $response->{nodeIds}->[0] = $childNodes[0]->{nodeId};
4206             #} else {
4207              
4208             #warn "Bad luck: Node with nodeId 0 found. Info for this one cannot be retrieved";
4209 0           $self->signal_condition( "Bad luck: Node with nodeId 0 found. Info for this one cannot be retrieved" );
4210             #};
4211             };
4212              
4213             # Resolve the found nodes directly with the
4214             # found node ids instead of returning the numbers and fetching
4215             # them later
4216             # We could also prefill some data with the results from
4217             # $childNodes here, if we have that?!
4218             # We build and search the document here:
4219 0           my %node_ids;
4220             #use Data::Dumper;
4221             #warn Dumper $doc;
4222 0           my @scan = @{ $doc->{children}};
  0            
4223 0           while( my $node = shift @scan ) {
4224 0           $node_ids{ $node->{nodeId}} = $node;
4225              
4226             #warn join ",", sort keys %node_ids;
4227 0 0         if( $node->{children} ) {
4228             unshift @scan,
4229 0           map { $_->{parentNodeId} = $node->{nodeId}; $_ }
  0            
4230 0           @{$node->{children}};
  0            
4231             };
4232             };
4233              
4234             #my @nodes = map {
4235             # WWW::Mechanize::Chrome::Node->fetchNode(
4236             # nodeId => 0+$_,
4237             # driver => $self->target,
4238             # );
4239             #} @{ $response->{nodeIds}};
4240             my @nodes = map {
4241 0           my $nid = $_;
4242             #my $request_f = $self->target->send_message('DOM.pushNodesByBackendIdsToFrontend',
4243             #backendNodeIds => [$node_ids{$_}->{backendNodeId}])
4244             #->then(sub( $info ) {
4245             # warn Dumper $info;
4246              
4247             # Convert the array of attributes to a hash of attributes ...
4248 0 0         if( ref $node_ids{$nid}->{attributes} eq 'ARRAY') {
4249             $node_ids{$nid}->{attributes} = +{
4250 0           @{ $node_ids{$nid}->{attributes} }
  0            
4251             };
4252             };
4253             Future->done(
4254             WWW::Mechanize::Chrome::Node->new(
4255 0           +{ %{$node_ids{$nid} },
  0            
4256             driver => $self->target,
4257             }
4258             ))
4259             #});
4260 0           } @{ $response->{nodeIds}};
  0            
4261              
4262 0           Future->wait_all( @nodes )
4263 0           })->then( sub( @fetched_nodes ) {
4264             # This should already happen through the DESTROY callback
4265             # but we'll be explicit here
4266 0           $setChildNodes->unregister;
4267 0           undef $setChildNodes;
4268              
4269             # Resolve the found nodes directly with the
4270             # found node ids instead of returning the numbers and fetching
4271             # them later
4272 0           my @foundNodes = map { $_->get() } @fetched_nodes;
  0            
4273 0           my $nodes = $self->_unwrapChildNodeTree( \@childNodes );
4274              
4275 0           for (@foundNodes) {
4276 0           my $id = $_->nodeId;
4277 0 0         if( ! defined $id ) {
4278             #use Data::Dumper;
4279             #warn "Found node without nodeId: " . Dumper $_;
4280             # Sometimes we get a spurious, empty node, so we ignore that
4281             # Maybe that is because the node we searched for went
4282             # away, but we'd need to associate the information
4283             # before we get the response, so ...
4284 0           next;
4285             };
4286             # Backfill here instead of overwriting!
4287 0 0         if( my $n = $nodes->{$id} ) {
4288 0           for my $key (qw( backendNodeId parentId )) {
4289 0           $_->{ $key } = $n->{ $key };
4290             };
4291 0 0         if( ! $_->{backendNodeId} ) {
4292 0           die "No backend node id found via " . Dumper $n;
4293             };
4294             };
4295 0           $nodes->{ $id } = $_;
4296             };
4297              
4298             # Filter @found for those nodes that have $nodeId as
4299             # ancestor because we can't restrict the search in Chrome
4300             # directly...
4301 0 0         if( $subTreeId ) {
4302              
4303 0           $self->log('trace', "Filtering query results for ancestor backendNodeId $subTreeId");
4304              
4305             # Find all nodes contained in our subtree
4306 0           my @scan = @{ $doc->{children}};
  0            
4307 0           my $subTree;
4308             my $inSubTree;
4309 0           my %foundNodes = map { $_->nodeId => $_ } @foundNodes;
  0            
4310 0           @foundNodes = ();
4311              
4312 0           while( my $node = shift @scan ) {
4313             #warn join ",", sort keys %node_ids;
4314              
4315 0 0         if( $node->{backendNodeId} == $subTreeId ) {
4316 0           $subTree = $node;
4317 0           $inSubTree = 1;
4318 0           @scan = @{$subTree->{children}};
  0            
4319 0           next;
4320             };
4321              
4322 0 0 0       if( $inSubTree and exists $foundNodes{ $node->{nodeId}}) {
4323 0           push @foundNodes, $foundNodes{ $node->{nodeId}};
4324             };
4325              
4326 0 0         if( $node->{children} ) {
4327             unshift @scan,
4328 0           map { $_->{parentNodeId} = $node->{nodeId}; $_ }
  0            
4329 0           @{$node->{children}};
  0            
4330             };
4331             };
4332              
4333 0           $self->log('debug', "filtered XPath query '$query' for ancestor $subTreeId (". (0+@foundNodes) . " node(s))");
4334             } else {
4335             #warn "*** Not filtering for any parent node";
4336             };
4337              
4338             # Downstream wants a double-nested Future, so do it here
4339             # until we fix downstream
4340 0           Future->wait_all( Future->done( @foundNodes ));
4341 0           });
4342             } else {
4343 0           return Future->done()
4344             };
4345 0           });
4346             }
4347              
4348 0     0 1   sub xpath( $self, $query, %options) {
  0            
  0            
  0            
  0            
4349 0 0 0       if ('ARRAY' ne (ref $query||'')) {
4350 0           $query = [$query];
4351             };
4352 0 0         if( not exists $options{ frames }) {
4353 0           $options{ frames }= $self->{frames};
4354             };
4355              
4356 0           my $single = $options{ single };
4357 0           my $first = $options{ one };
4358 0           my $maybe = $options{ maybe };
4359 0           my $any = $options{ any };
4360 0   0       my $index = $options{ index } || 0;
4361 0 0         if( $index >= 1 ) {
4362 0           $index--;
4363             };
4364 0   0       my $return_first_element = ($single or $first or $maybe or $any );
4365 0   0       $options{ user_info }||= join "|", @$query;
4366              
4367             # Construct some helper variables
4368 0   0       my $zero_allowed = not ($single or $first);
4369 0   0       my $two_allowed = (not( $single or $maybe)) || defined $options{ index };
4370              
4371             # Sanity check for the common error of
4372             # my $item = $mech->xpath("//foo");
4373 0 0 0       if (! exists $options{ all } and not ($return_first_element)) {
4374 0 0 0       $self->signal_condition(join "\n",
4375             "You asked for many elements but seem to only want a single item.",
4376             "Did you forget to pass the 'single' option with a true value?",
4377             "Pass 'all => 1' to suppress this message and receive the count of items.",
4378             ) if defined wantarray and !wantarray;
4379             };
4380              
4381 0           my @res;
4382              
4383 0 0         if( $options{ document }) {
4384 0           warn sprintf "Document %s", $options{ document }->{id};
4385             };
4386              
4387             #my $doc= $options{ document } ? Future->done( $options{ document } ) : $self->document_future;
4388 0           my $doc = Future->done();
4389              
4390 0           weaken(my $s = $self);
4391              
4392             $doc->then( sub {
4393 0     0     my $q = join "|", @$query;
4394              
4395 0           my @found;
4396             my $id;
4397 0 0         if ($options{ node }) {
4398 0           $id = $options{ node }->backendNodeId;
4399             #warn "Performing search (below '$id')";
4400             } else {
4401             #warn "Performing search across complete DOM";
4402             };
4403             Future->wait_all(
4404             map {
4405 0           $s->_performSearch( query => $_, subTreeId => $id )
  0            
4406             } @$query
4407             );
4408             })->then( sub {
4409 0 0   0     my @found = map { my @r = $_->get; @r ? map { $_->get } @r : () } @_;
  0            
  0            
  0            
4410             #for( @found ) {
4411             # use Data::Dumper;
4412             # warn "Found " . Dumper $_;
4413             #};
4414 0           push @res, @found;
4415 0           Future->done( 1 );
4416 0           })->get;
4417              
4418             # Determine if we want only one element
4419             # or a list, like WWW::Mechanize::Chrome
4420              
4421 0 0 0       if (! $zero_allowed and @res == 0) {
4422 0           $self->signal_condition( sprintf "No elements found for %s", $options{ user_info } );
4423             };
4424 0 0 0       if (! $two_allowed and @res > 1) {
4425             #$self->highlight_node(@res);
4426 0   0       warn $_->get_text() || '<no text>' for @res;
4427 0           $self->signal_condition( sprintf "%d elements found for %s", (scalar @res), $options{ user_info } );
4428             };
4429              
4430 0 0         $return_first_element ? $res[$index] : @res
4431             }
4432              
4433             =head2 C<< $mech->by_id( $id, %options ) >>
4434              
4435             my @text = $mech->by_id('_foo:bar');
4436              
4437             Returns all nodes matching the given ids. If
4438             C<$id> is an array reference, it returns
4439             all nodes matched by any of the ids in the array.
4440              
4441             This method is equivalent to calling C<< ->xpath >> :
4442              
4443             $self->xpath(qq{//*[\@id="$_"]}, %options)
4444              
4445             It is convenient when your element ids get mistaken for
4446             CSS selectors.
4447              
4448             =cut
4449              
4450             sub by_id {
4451 0     0 1   my ($self,$query,%options) = @_;
4452 0 0 0       if ('ARRAY' ne (ref $query||'')) {
4453 0           $query = [$query];
4454             };
4455             $options{ user_info } ||= "id "
4456 0   0       . join(" or ", map {qq{'$_'}} @$query)
  0            
4457             . " found";
4458 0           $query = [map { qq{.//*[\@id="$_"]} } @$query];
  0            
4459 0           $self->xpath($query, %options)
4460             }
4461              
4462             =head2 C<< $mech->click( $name [,$x ,$y] ) >>
4463              
4464             # If the element is within a <form> element
4465             $mech->click( 'go' );
4466              
4467             # If the element is anywhere on the page
4468             $mech->click({ xpath => '//button[@name="go"]' });
4469              
4470             Has the effect of clicking a button (or other element) on the current form. The
4471             first argument is the C<name> of the button to be clicked. The second and third
4472             arguments (optional) allow you to specify the (x,y) coordinates of the click.
4473              
4474             If there is only one button on the form, C<< $mech->click() >> with
4475             no arguments simply clicks that one button.
4476              
4477             If you pass in a hash reference instead of a name,
4478             the following keys are recognized:
4479              
4480             =over 4
4481              
4482             =item *
4483              
4484             C<text> - Find the element to click by its contained text
4485              
4486             =item *
4487              
4488             C<selector> - Find the element to click by the CSS selector
4489              
4490             =item *
4491              
4492             C<xpath> - Find the element to click by the XPath query
4493              
4494             =item *
4495              
4496             C<dom> - Click on the passed DOM element
4497              
4498             You can use this to click on arbitrary page elements. There is no convenient
4499             way to pass x/y co-ordinates when using the C<dom> option.
4500              
4501             =item *
4502              
4503             C<id> - Click on the element with the given id
4504              
4505             This is useful if your document ids contain characters that
4506             do look like CSS selectors. It is equivalent to
4507              
4508             xpath => qq{//*[\@id="$id"]}
4509              
4510             =item *
4511              
4512             C<intrapage> - Override the detection of whether to wait for a HTTP response
4513             or not. Setting this will never wait for an HTTP response.
4514              
4515             =back
4516              
4517             Returns a L<HTTP::Response> object.
4518              
4519             As a deviation from the WWW::Mechanize API, you can also pass a
4520             hash reference as the first parameter. In it, you can specify
4521             the parameters to search much like for the C<find_link> calls.
4522              
4523             =cut
4524              
4525             sub click {
4526 0     0 1   my ($self,$name,$x,$y) = @_;
4527 0           my %options;
4528             my @buttons;
4529              
4530 0 0 0       if (! defined $name) {
    0 0        
    0          
4531 0           croak("->click called with undef link");
4532             } elsif (ref $name and blessed $name and $name->isa('WWW::Mechanize::Chrome::Node') ) {
4533 0           $options{ dom } = $name;
4534             } elsif (ref $name eq 'HASH') { # options
4535 0           %options = %$name;
4536             } else {
4537 0           $options{ name } = $name;
4538             };
4539              
4540 0 0         if( exists $options{ text }) {
4541 0           $options{ xpath } = sprintf q{//*[text() = "%s"]}, quote_xpath( $options{ text });
4542             };
4543              
4544 0 0         if (exists $options{ name }) {
4545 0   0       $name = quotemeta($options{ name }|| '');
4546             $options{ xpath } = [
4547 0           sprintf( q{//*[(translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="button" and @name="%s") or (translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="input" and (@type="button" or @type="submit" or @type="image") and @name="%s")]}, $name, $name),
4548             ];
4549 0 0         if ($options{ name } eq '') {
4550 0           push @{ $options{ xpath }},
  0            
4551             q{//*[(translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "button" or translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="input") and @type="button" or @type="submit" or @type="image"]},
4552             ;
4553             };
4554 0           $options{ user_info } = "Button with name '$name'";
4555             };
4556              
4557 0 0         if ($options{ dom }) {
4558 0           @buttons = $options{ dom };
4559             } else {
4560 0           @buttons = $self->_option_query(%options);
4561             };
4562              
4563             # Get the node as an object so we can find its position and send the clicks:
4564 0           $self->log('trace', sprintf "Resolving nodeId %d to object for clicking", $buttons[0]->nodeId );
4565 0           my $id = $buttons[0]->objectId;
4566             #warn Dumper $self->target->send_message('Runtime.getProperties', objectId => $id)->get;
4567             #warn Dumper $self->target->send_message('Runtime.callFunctionOn', objectId => $id, functionDeclaration => 'function() { this.focus(); }', arguments => [])->get;
4568              
4569             $self->_mightNavigate( sub {
4570 0     0     $self->target->send_message('Runtime.callFunctionOn', objectId => $id, functionDeclaration => 'function() { this.click(); }', arguments => [])
4571 0           }, %options)
4572             ->get;
4573              
4574 0           return $self->response;
4575             }
4576              
4577             # Internal method to run either an XPath, CSS or id query against the DOM
4578             # Returns the element(s) found
4579             my %rename = (
4580             xpath => 'xpath',
4581             selector => 'selector',
4582             id => 'by_id',
4583             by_id => 'by_id',
4584             );
4585              
4586             sub _option_query {
4587 0     0     my ($self,%options) = @_;
4588 0           my ($method,$q);
4589 0           for my $meth (keys %rename) {
4590 0 0         if (exists $options{ $meth }) {
4591 0           $q = delete $options{ $meth };
4592 0   0       $method = $rename{ $meth } || $meth;
4593             }
4594             };
4595 0           _default_limiter( 'one' => \%options );
4596 0 0         croak "Need either a name, a selector or an xpath key!"
4597             if not $method;
4598 0           return $self->$method( $q, %options );
4599             };
4600              
4601             # Return the default limiter if no other limiting option is set:
4602             sub _default_limiter {
4603 0     0     my ($default, $options) = @_;
4604 0 0         if (! grep { exists $options->{ $_ } } qw(single one maybe all any)) {
  0            
4605 0           $options->{ $default } = 1;
4606             };
4607             return ()
4608 0           };
4609              
4610             =head2 C<< $mech->click_button( ... ) >>
4611              
4612             $mech->click_button( name => 'go' );
4613             $mech->click_button( input => $mybutton );
4614              
4615             Has the effect of clicking a button on the current form by specifying its
4616             name, value, or index. Its arguments are a list of key/value pairs. Only
4617             one of name, number, input or value must be specified in the keys.
4618              
4619             =over 4
4620              
4621             =item *
4622              
4623             C<name> - name of the button
4624              
4625             =item *
4626              
4627             C<value> - value of the button
4628              
4629             =item *
4630              
4631             C<input> - DOM node
4632              
4633             =item *
4634              
4635             C<id> - id of the button
4636              
4637             =item *
4638              
4639             C<number> - number of the button
4640              
4641             =back
4642              
4643             If you find yourself wanting to specify a button through its
4644             C<selector> or C<xpath>, consider using C<< ->click >> instead.
4645              
4646             =cut
4647              
4648 0     0 1   sub click_button($self,%options) {
  0            
  0            
  0            
4649 0           my $node;
4650             my $xpath;
4651 0           my $user_message;
4652 0 0         if (exists $options{ input }) {
    0          
    0          
    0          
    0          
4653 0           $node = delete $options{ input };
4654             } elsif (exists $options{ name }) {
4655 0           my $v = delete $options{ name };
4656 0           $xpath = sprintf( '//*[(translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "button" and @name="%s") or (translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="input" and @type="button" or @type="submit" and @name="%s")]', $v, $v);
4657 0           $user_message = "Button name '$v' unknown";
4658             } elsif (exists $options{ value }) {
4659 0           my $v = delete $options{ value };
4660 0           $xpath = sprintf( '//*[(translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "button" and @value="%s") or (translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="input" and (@type="button" or @type="submit") and @value="%s")]', $v, $v);
4661 0           $user_message = "Button value '$v' unknown";
4662             } elsif (exists $options{ id }) {
4663 0           my $v = delete $options{ id };
4664 0           $xpath = sprintf '//*[@id="%s"]', $v;
4665 0           $user_message = "Button id '$v' unknown";
4666             } elsif (exists $options{ number }) {
4667 0           my $v = delete $options{ number };
4668 0           $xpath = sprintf '//*[translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "button" or (translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "input" and @type="submit")][%s]', $v;
4669 0           $user_message = "Button number '$v' out of range";
4670             };
4671 0   0       $node ||= $self->xpath( $xpath,
4672             node => $self->current_form,
4673             single => 1,
4674             user_message => $user_message,
4675             );
4676 0 0         if ($node) {
4677 0           $self->click({ dom => $node, %options });
4678             } else {
4679              
4680 0           $self->signal_condition($user_message);
4681             };
4682              
4683             }
4684              
4685             =head1 FORM METHODS
4686              
4687             =head2 C<< $mech->current_form() >>
4688              
4689             print $mech->current_form->{name};
4690              
4691             Returns the current form.
4692              
4693             This method is incompatible with L<WWW::Mechanize>.
4694             It returns the DOM C<< <form> >> object and not
4695             a L<HTML::Form> instance.
4696              
4697             The current form will be reset by WWW::Mechanize::Chrome
4698             on calls to C<< ->get() >> and C<< ->get_local() >>,
4699             and on calls to C<< ->submit() >> and C<< ->submit_with_fields >>.
4700              
4701             =cut
4702              
4703             sub current_form {
4704 0     0 1   my( $self, %options )= @_;
4705             # Find the first <FORM> element from the currently active element
4706 0 0         $self->form_number(1) unless $self->{current_form};
4707 0           $self->{current_form};
4708             }
4709              
4710             sub clear_current_form {
4711 0     0 0   undef $_[0]->{current_form};
4712             };
4713              
4714 0     0 0   sub invalidate_cached_values($self) {
  0            
  0            
4715 0           $self->clear_current_form;
4716 0           $self->_clear_cached_document;
4717             }
4718              
4719             sub active_form {
4720 0     0 0   my( $self, %options )= @_;
4721             # Find the first <FORM> element from the currently active element
4722 0           my $focus= $self->target->get_active_element;
4723              
4724 0 0         if( !$focus ) {
4725 0           warn "No active element, hence no active form";
4726             return
4727 0           };
4728              
4729 0           my $form= $self->xpath( './ancestor-or-self::FORM', node => $focus, maybe => 1 );
4730              
4731             }
4732              
4733             =head2 C<< $mech->dump_forms( [$fh] ) >>
4734              
4735             open my $fh, '>', 'form-log.txt'
4736             or die "Couldn't open logfile 'form-log.txt': $!";
4737             $mech->dump_forms( $fh );
4738              
4739             Prints a dump of the forms on the current page to
4740             the filehandle C<$fh>. If C<$fh> is not specified or is undef, it dumps
4741             to C<STDOUT>.
4742              
4743             =cut
4744              
4745             sub dump_forms {
4746 0     0 1   my $self = shift;
4747 0   0       my $fh = shift || \*STDOUT;
4748              
4749 0           for my $form ( $self->forms ) {
4750 0   0       print {$fh} "[FORM] ", $form->get_attribute('name', live => 1) || '<no name>', ' ', $form->get_attribute('action'), "\n";
  0            
4751             #for my $f ($self->xpath( './/*', node => $form )) {
4752             #for my $f ($self->xpath( './/*[contains(" "+translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")+" "," input textarea button select "
4753             # )]', node => $form )) {
4754 0           for my $f ($self->xpath( './/*[contains(" input textarea button select ",concat(" ",translate(local-name(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")," "))]', node => $form )) {
4755 0           my $type;
4756 0 0 0       if($type= $f->get_attribute('type', live => 1) || '' ) {
4757 0           $type= " ($type)";
4758             };
4759              
4760 0   0       print {$fh} " [", $f->get_attribute('tagName', live => 1), $type, "] ", $f->get_attribute('name') || '<no name>', "\n";
  0            
4761             };
4762             }
4763 0           return;
4764             }
4765              
4766             =head2 C<< $mech->form_name( $name [, %options] ) >>
4767              
4768             $mech->form_name( 'search' );
4769              
4770             Selects the current form by its name. The options
4771             are identical to those accepted by the L<< /$mech->xpath >> method.
4772              
4773             =cut
4774              
4775             sub form_name {
4776 0     0 1   my ($self,$name,%options) = @_;
4777 0           $name = quote_xpath( $name );
4778 0           _default_limiter( single => \%options );
4779 0           $self->{current_form} = $self->selector("form[name='$name']",
4780             user_info => "form name '$name'",
4781             %options
4782             );
4783             };
4784              
4785             =head2 C<< $mech->form_id( $id [, %options] ) >>
4786              
4787             $mech->form_id( 'login' );
4788              
4789             Selects the current form by its C<id> attribute.
4790             The options
4791             are identical to those accepted by the L<< /$mech->xpath >> method.
4792              
4793             This is equivalent to calling
4794              
4795             $mech->by_id($id,single => 1,%options)
4796              
4797             =cut
4798              
4799             sub form_id {
4800 0     0 1   my ($self,$name,%options) = @_;
4801              
4802 0           _default_limiter( single => \%options );
4803 0           $self->{current_form} = $self->by_id($name,
4804             user_info => "form with id '$name'",
4805             %options
4806             );
4807             };
4808              
4809             =head2 C<< $mech->form_number( $number [, %options] ) >>
4810              
4811             $mech->form_number( 2 );
4812              
4813             Selects the I<number>th form.
4814             The options
4815             are identical to those accepted by the L<< /$mech->xpath >> method.
4816              
4817             =cut
4818              
4819             sub form_number {
4820 0     0 1   my ($self,$number,%options) = @_;
4821              
4822 0           _default_limiter( single => \%options );
4823 0           $self->{current_form} = $self->xpath("(//form)[$number]",
4824             user_info => "form number $number",
4825             %options
4826             );
4827 0           $self->{current_form};
4828             };
4829              
4830             =head2 C<< $mech->form_with_fields( [$options], @fields ) >>
4831              
4832             $mech->form_with_fields(
4833             'user', 'password'
4834             );
4835              
4836             Find the form which has the listed fields.
4837              
4838             If the first argument is a hash reference, it's taken
4839             as options to C<< ->xpath >>.
4840              
4841             See also L<< /$mech->submit_form >>.
4842              
4843             =cut
4844              
4845             sub form_with_fields {
4846 0     0 1   my ($self,@fields) = @_;
4847 0           my $options = {};
4848 0 0         if (ref $fields[0] eq 'HASH') {
4849 0           $options = shift @fields;
4850             };
4851 0           my @clauses = map { $self->element_query([qw[input select textarea]], { 'name' => $_ })} @fields;
  0            
4852              
4853 0           my $q = "//form[" . join( " and ", @clauses)."]";
4854             #warn $q;
4855 0           _default_limiter( single => $options );
4856 0           $self->{current_form} = $self->xpath($q,
4857             user_info => "form with fields [@fields]",
4858             %$options
4859             );
4860             #warn $form;
4861 0           $self->{current_form};
4862             };
4863              
4864             =head2 C<< $mech->forms( %options ) >>
4865              
4866             my @forms = $mech->forms();
4867              
4868             When called in a list context, returns a list
4869             of the forms found in the last fetched page.
4870             In a scalar context, returns a reference to
4871             an array with those forms.
4872              
4873             The options
4874             are identical to those accepted by the L<< /$mech->selector >> method.
4875              
4876             The returned elements are the DOM C<< <form> >> elements.
4877              
4878             =cut
4879              
4880             sub forms {
4881 0     0 1   my ($self, %options) = @_;
4882 0           my @res = $self->selector('form', %options);
4883             return wantarray ? @res
4884 0 0         : \@res
4885             };
4886              
4887             =head2 C<< $mech->field( $selector, $value, [, $index, \@pre_events [,\@post_events]] ) >>
4888              
4889             $mech->field( user => 'joe' );
4890             $mech->field( not_empty => '', 0, [], [] ); # bypass JS validation
4891             $mech->field( date => '2020-04-01', 2 ); # set second field named "date"
4892              
4893             Sets the field with the name given in C<$selector> to the given value.
4894             Returns the value.
4895              
4896             The method understands very basic CSS selectors in the value for C<$selector>,
4897             like the L<HTML::Form> find_input() method.
4898              
4899             A selector prefixed with '#' must match the id attribute of the input.
4900             A selector prefixed with '.' matches the class attribute. A selector
4901             prefixed with '^' or with no prefix matches the name attribute.
4902              
4903             By passing the array reference C<@pre_events>, you can indicate which
4904             Javascript events you want to be triggered before setting the value.
4905             C<@post_events> contains the events you want to be triggered
4906             after setting the value.
4907              
4908             By default, the events set in the
4909             constructor for C<pre_events> and C<post_events>
4910             are triggered.
4911              
4912             =cut
4913              
4914 0     0 1   sub field($self,$name,$value,$index=undef,$pre=undef,$post=undef) {
  0            
  0            
  0            
  0            
  0            
  0            
  0            
4915 0 0         if( ref $index ) { # old API
4916 0           carp "Old API style for ->field() is deprecated. Please fix the call to pass undef for the third parameter if using pre_events/post_events!";
4917 0           $post = $pre;
4918 0           $pre = $index;
4919 0           $index = undef;
4920             };
4921 0           $self->get_set_value(
4922             name => $name,
4923             value => $value,
4924             pre => $pre,
4925             post => $post,
4926             index => $index,
4927             node => $self->current_form,
4928             );
4929             }
4930              
4931             =head2 C<< $mech->sendkeys( %options ) >>
4932              
4933             $mech->sendkeys( string => "Hello World" );
4934              
4935             Sends a series of keystrokes. The keystrokes can be either a string or a
4936             reference to an array containing the detailed data as hashes.
4937              
4938             =over 4
4939              
4940             =item B<string> - the string to send as keystrokes
4941              
4942             =item B<keys> - reference of the array to send as keystrokes
4943              
4944             =item B<delay> - delay in ms to sleep between keys
4945              
4946             =back
4947              
4948             =cut
4949              
4950 0     0 0   sub sendkeys_future( $self, %options ) {
  0            
  0            
  0            
4951             $options{ keys } ||= [ map +{ type => 'char', text => $_ },
4952             split m//, $options{ string }
4953 0   0       ];
4954              
4955 0           my $f = Future->done(1);
4956              
4957 0           for my $key (@{ $options{ keys }}) {
  0            
4958             $f = $f->then(sub {
4959 0     0     $self->target->send_message('Input.dispatchKeyEvent', %$key );
4960 0           });
4961 0 0         if( defined $options{ delay }) {
4962             $f->then(sub {
4963 0     0     $self->sleep( $options{ delay });
4964 0           });
4965             };
4966             };
4967              
4968 0           return $f
4969             };
4970              
4971 0     0 1   sub sendkeys( $self, %options ) {
  0            
  0            
  0            
4972 0           $self->sendkeys_future( %options )->get
4973             }
4974              
4975             =head2 C<< $mech->upload( $selector, $value ) >>
4976              
4977             $mech->upload( user_picture => 'C:/Users/Joe/face.png' );
4978              
4979             Sets the file upload field with the name given in C<$selector> to the given
4980             file. The filename must be an absolute path and filename in the local
4981             filesystem.
4982              
4983             The method understands very basic CSS selectors in the value for C<$selector>,
4984             like the C<< ->field >> method.
4985              
4986             =cut
4987              
4988             # Page.setInterceptFileChooserDialog
4989             # doesn't help anything, since we can only suppress that dialog but not
4990             # supply file names or anything. See the ->upload() method for how to actually
4991             # set filenames
4992              
4993 0     0 1   sub upload($self,$name,$value) {
  0            
  0            
  0            
  0            
4994 0           my %options;
4995              
4996 0           my @fields = $self->_field_by_name(
4997             name => $name,
4998             user_info => "upload field with name '$name'",
4999             %options );
5000 0 0         $value = [$value]
5001             if ! ref $value;
5002              
5003             # Stringify all files:
5004 0           @$value = map { "$_" } @$value;
  0            
5005              
5006 0 0         if( @fields ) {
5007 0           $self->target->send_message('DOM.setFileInputFiles',
5008             nodeId => 0+$fields[0]->nodeId,
5009             files => $value,
5010             )->get;
5011             }
5012              
5013             }
5014              
5015              
5016             =head2 C<< $mech->value( $selector_or_element, [ $index | %options] ) >>
5017              
5018             print $mech->value( 'user' );
5019              
5020             Returns the value of the field given by C<$selector_or_name> or of the
5021             DOM element passed in.
5022              
5023             If you have multiple fields with the same name, you can use the index
5024             to specify the index directly:
5025              
5026             print $mech->value( 'date', 2 ); # get the second field named "date"
5027              
5028             The legacy form of
5029              
5030             $mech->value( name => value );
5031              
5032             is not supported anymore.
5033              
5034             For fields that can have multiple values, like a C<select> field,
5035             the method is context sensitive and returns the first selected
5036             value in scalar context and all values in list context.
5037              
5038             Note that this method does not support file uploads. See the C<< ->upload >>
5039             method for that.
5040              
5041             =cut
5042              
5043             sub value {
5044 0 0   0 1   if (@_ == 3) {
5045 0           my ($self,$name,$index) = @_;
5046              
5047 0 0 0       if( defined $index and $index !~ /^\d+$/ ) {
5048 0           $self->signal_condition("Non-numeric index passed to ->value(). Did you mean to call ->field('$name' => '$index') ?");
5049             };
5050              
5051 0           return $self->get_set_value(
5052             node => $self->current_form,
5053             index => $index,
5054             name => $name,
5055             );
5056              
5057             } else {
5058 0           my ($self,$name,%options) = @_;
5059 0           return $self->get_set_value(
5060             node => $self->current_form,
5061             %options,
5062             name => $name,
5063             );
5064             };
5065             };
5066              
5067             =head2 C<< $mech->get_set_value( %options ) >>
5068              
5069             Allows fine-grained access to getting/setting a value
5070             with a different API. Supported keys are:
5071              
5072             name
5073             value
5074             pre
5075             post
5076              
5077             in addition to all keys that C<< $mech->xpath >> supports.
5078              
5079             =cut
5080              
5081             sub _field_by_name {
5082 0     0     my ($self,%options) = @_;
5083 0           my @fields;
5084 0           my $name = delete $options{ name };
5085 0           my $attr = 'name';
5086 0 0         if ($name =~ s/^\^//) { # if it starts with ^, it's supposed to be a name
    0          
    0          
5087 0           $attr = 'name'
5088             } elsif ($name =~ s/^#//) {
5089 0           $attr = 'id'
5090             } elsif ($name =~ s/^\.//) {
5091 0           $attr = 'class'
5092             };
5093 0 0         if (blessed $name) {
5094 0           @fields = $name;
5095             } else {
5096 0           _default_limiter( single => \%options );
5097 0           my $query = $self->element_query([qw[input select textarea]], { $attr => $name });
5098 0           @fields = $self->xpath($query,%options);
5099             };
5100             @fields
5101 0           }
5102              
5103             =head2 C<< $mech->set_field( %options ) >>
5104              
5105             $mech->set_field(
5106             field => $field_node,
5107             value => 'foo',
5108             );
5109              
5110             Low level value setting method. Use this if you have an input element outside
5111             of a E<lt>formE<gt> tag.
5112              
5113             =cut
5114              
5115 0     0 1   sub set_field($self, %options ) {
  0            
  0            
  0            
5116 0           my $value = delete $options{ value };
5117 0           my $pre = delete $options{pre};
5118 0 0 0       $pre = [$pre]
5119             if (defined $pre and ! ref $pre);
5120 0           my $post = delete $options{post};
5121 0 0 0       $post = [$post]
5122             if (defined $post and ! ref $post);
5123 0   0       $pre ||= ['focus']; # just to eliminate some checks downwards
5124 0   0       $post ||= ['change']; # just to eliminate some checks downwards
5125             my $obj = delete $options{ field }
5126 0 0         or croak "Need a field to set";
5127 0           my $tag = $obj->get_tag_name();
5128              
5129 0           my %method = (
5130             input => 'value',
5131             textarea => 'content',
5132             select => 'selected',
5133             );
5134 0           my $method = $method{ lc $tag };
5135 0 0 0       if( lc $tag eq 'input' and $obj->get_attribute('type', live => 1) eq 'radio' ) {
5136 0           $method = 'checked';
5137             };
5138              
5139 0           my $id = $obj->objectId;
5140 0 0         if( ! $id ) {
5141 0           warn "No object id for nodeId " . $obj->nodeId;
5142             };
5143              
5144             # Send pre-change events:
5145 0           for my $ev (@$pre) {
5146 0           $self->target->send_message(
5147             'Runtime.callFunctionOn',
5148             objectId => $id,
5149             functionDeclaration => <<'JS',
5150             function(ev) {
5151             var event = new Event(ev, {
5152             view : window,
5153             bubbles: true,
5154             cancelable: true
5155             });
5156             this.dispatchEvent(event);
5157             }
5158             JS
5159             arguments => [{ value => $ev }],
5160             );
5161             };
5162              
5163 0 0         if( 'value' eq $method ) {
    0          
    0          
    0          
5164 0           $self->target->send_message('DOM.setAttributeValue', nodeId => 0+$obj->nodeId, name => 'value', value => "$value" )->get;
5165              
5166             } elsif( 'selected' eq $method ) {
5167             # ignoring undef; but [] would reset to no option
5168 0 0         if (defined $value) {
5169              
5170 0 0         $value = [ $value ] unless ref $value;
5171 0           $self->target->send_message(
5172             'Runtime.callFunctionOn',
5173             objectId => $id,
5174             functionDeclaration => <<'JS',
5175             function(newValue) {
5176             var i, j;
5177             if (this.multiple == true) {
5178             for (i=0; i<this.options.length; i++) {
5179             this.options[i].selected = false
5180             }
5181             }
5182             for (j=0; j<newValue.length; j++) {
5183             for (i=0; i<this.options.length; i++) {
5184             if (this.options[i].value == newValue[j]) {
5185             this.options[i].selected = true
5186             }
5187             }
5188             }
5189             }
5190             JS
5191             arguments => [{ value => $value }],
5192             )->get;
5193             }
5194             } elsif( 'checked' eq $method ) {
5195 0 0         if (defined $value) {
5196 0 0         $value = [ $value ] unless ref $value;
5197 0           $obj->set_attribute('checked' => JSON::true);
5198             }
5199             } elsif( 'content' eq $method ) {
5200 0           $self->target->send_message('Runtime.callFunctionOn',
5201             objectId => $id,
5202             functionDeclaration => 'function(newValue) { this.innerHTML = newValue }',
5203             arguments => [{ value => $value }]
5204             )->get;
5205             } else {
5206 0           die "Don't know how to set the value for node '$tag', sorry";
5207             };
5208              
5209             # Send post-change events
5210             # Send pre-change events:
5211 0           for my $ev (@$post) {
5212 0           $self->target->send_message(
5213             'Runtime.callFunctionOn',
5214             objectId => $id,
5215             functionDeclaration => <<'JS',
5216             function(ev) {
5217             var event = new Event(ev, {
5218             view : window,
5219             bubbles: true,
5220             cancelable: true
5221             });
5222             this.dispatchEvent(event);
5223             }
5224             JS
5225             arguments => [{ value => $ev }],
5226             );
5227             };
5228             }
5229              
5230 0     0 1   sub get_set_value($self,%options) {
  0            
  0            
  0            
5231 0           my $set_value = exists $options{ value };
5232 0           my $value = delete $options{ value };
5233 0           my $pre = delete $options{pre};
5234 0 0 0       $pre = [$pre]
5235             if (defined $pre and ! ref $pre);
5236 0           my $post = delete $options{post};
5237 0 0 0       $post = [$post]
5238             if (defined $post and ! ref $post);
5239 0   0       $pre ||= ['focus']; # just to eliminate some checks downwards
5240 0   0       $post ||= ['change']; # just to eliminate some checks downwards
5241 0           my $name = delete $options{ name };
5242 0           my $index = delete $options{ index };
5243              
5244 0           my $index_name = '';
5245 0 0         if( defined $index ) {
5246 0 0 0       if( $index == 1 or $index =~ /[^1]1$/ ) {
    0 0        
    0 0        
5247 0           $index_name = "${index}st ";
5248              
5249             } elsif( $index == 2 or $index =~ /[^1]2$/ ) {
5250 0           $index_name = "${index}nd ";
5251              
5252             } elsif( $index == 3 or $index =~ /[^1]3$/ ) {
5253 0           $index_name = "${index}rd ";
5254              
5255             } else {
5256 0           $index_name = "${index}th ";
5257             }
5258             };
5259 0           my @fields = $self->_field_by_name(
5260             name => $name,
5261             user_info => "${index_name}input with name '$name'",
5262             index => $index,
5263             %options );
5264              
5265 0 0         if (my $obj = $fields[0]) {
5266              
5267 0 0         if ($set_value) {
5268 0           $self->set_field(
5269             field => $obj,
5270             value => $value,
5271             pre => $pre,
5272             post => $post,
5273             );
5274             };
5275              
5276             # Don't bother to fetch the field's value if it's not wanted
5277 0 0         return unless defined wantarray;
5278              
5279             # We could save some work here for the simple case of single-select
5280             # dropdowns by not enumerating all options
5281 0           my $tag = $obj->get_tag_name();
5282 0 0         if ('SELECT' eq uc $tag) {
5283 0           my $id = $obj->objectId;
5284 0 0         if( ! $id ) {
5285 0           warn "No object id for nodeId " . $obj->nodeId;
5286             };
5287             my $arr = $self->target->send_message(
5288             'Runtime.callFunctionOn',
5289             objectId => $id,
5290             functionDeclaration => <<'JS',
5291             function() {
5292             var i;
5293             var arr = [];
5294             for (i=0; i<this.options.length; i++) {
5295             if (this.options[i].selected == true) {
5296             arr.push(this.options[i].value);
5297             }
5298             }
5299             return arr;
5300             }
5301             JS
5302             arguments => [],
5303 0           returnByValue => JSON::true)->get->{result};
5304              
5305 0           my @values = @{$arr->{value}};
  0            
5306 0 0         if (wantarray) {
5307             return @values
5308 0           } else {
5309 0           return $values[0];
5310             }
5311             } else {
5312 0           return $obj->get_attribute('value', live => 1);
5313             };
5314             } else {
5315             return
5316 0           }
5317             }
5318              
5319             =head2 C<< $mech->select( $name, $value ) >>
5320              
5321             =head2 C<< $mech->select( $name, \@values ) >>
5322              
5323             $mech->select( 'items', 'banana' );
5324              
5325             Given the name of a C<select> field, set its value to the value
5326             specified. If the field is not C<< <select multiple> >> and the
5327             C<$value> is an array, only the B<first> value will be set.
5328             Passing C<$value> as a hash with
5329             an C<n> key selects an item by number (e.g.
5330             C<< {n => 3} >> or C<< {n => [2,4]} >>).
5331             The numbering starts at 1. This applies to the current form.
5332              
5333             If you have a field with C<< <select multiple> >> and you pass a single
5334             C<$value>, then C<$value> will be added to the list of fields selected,
5335             without clearing the others. However, if you pass an array reference,
5336             then all previously selected values will be cleared.
5337              
5338             Returns true on successfully setting the value. On failure, returns
5339             false and calls C<< $self>warn() >> with an error message.
5340              
5341             =cut
5342              
5343 0     0 1   sub select($self, $name, $value) {
  0            
  0            
  0            
  0            
5344 0           my $field;
5345 0 0         if( ! eval {
5346 0           ($field) = $self->_field_by_name(
5347             node => $self->current_form,
5348             name => $name,
5349             #%options,
5350             );
5351 0           1;
5352             }) {
5353             # the field was not found
5354 0           return;
5355             };
5356              
5357 0           my @options = $self->xpath( './/option', node => $field);
5358 0           my @by_index;
5359             my @by_value;
5360 0           my $single = $field->get_attribute('type', live => 1) eq "select-one";
5361 0           my $deselect;
5362              
5363 0 0 0       if ('HASH' eq ref $value||'') {
    0 0        
5364 0           for (keys %$value) {
5365 0 0         $self->warn(qq{Unknown select value parameter "$_"})
5366             unless $_ eq 'n';
5367             }
5368              
5369 0           $deselect = ref $value->{n};
5370 0 0         @by_index = ref $value->{n} ? @{ $value->{n} } : $value->{n};
  0            
5371             } elsif ('ARRAY' eq ref $value||'') {
5372             # clear all preselected values
5373 0           $deselect = 1;
5374 0           @by_value = @{ $value };
  0            
5375             } else {
5376 0           @by_value = $value;
5377             };
5378              
5379 0 0         if ($deselect) {
5380 0           for my $o (@options) {
5381 0           $o->{selected} = 0;
5382             }
5383             };
5384              
5385 0 0         if ($single) {
5386             # Only use the first element for single-element boxes
5387 0 0         $#by_index = 0+@by_index ? 0 : -1;
5388 0 0         $#by_value = 0+@by_value ? 0 : -1;
5389             };
5390              
5391             # Select the items, either by index or by value
5392 0           for my $idx (@by_index) {
5393 0           $options[$idx-1]->set_attribute('selected' => 1 );
5394             };
5395              
5396 0           for my $v (@by_value) {
5397 0           my $option = $self->xpath( sprintf( './/option[@value="%s"]', quote_xpath( $v )) , node => $field, single => 1 );
5398 0           $option->set_attribute( 'selected' => '1' );
5399             };
5400              
5401 0           return @by_index + @by_value > 0;
5402             }
5403              
5404             =head2 C<< $mech->tick( $name, $value [, $set ] ) >>
5405              
5406             $mech->tick("confirmation_box", 'yes');
5407              
5408             "Ticks" the first checkbox that has both the name and value associated with it
5409             on the current form. Dies if there is no named check box for that value.
5410             Passing in a false value as the third optional argument will cause the
5411             checkbox to be unticked.
5412              
5413             (Un)ticking the checkbox is done by sending a click event to it if needed.
5414             If C<$value> is C<undef>, the first checkbox matching C<$name> will
5415             be (un)ticked.
5416              
5417             If C<$name> is a reference to a hash, that hash will be used
5418             as the options to C<< ->find_link_dom >> to find the element.
5419              
5420             =cut
5421              
5422 0     0 1   sub tick($self, $name, $value=undef, $set=1) {
  0            
  0            
  0            
  0            
  0            
5423 0           my %options;
5424             my @boxes;
5425              
5426 0 0 0       if (! defined $name) {
    0          
    0          
5427 0           croak("->tick called with undef name");
5428             } elsif (ref $name and blessed($name)) {
5429 0           $options{ dom } = $name;
5430             } elsif (ref $name eq 'HASH') { # options
5431 0           %options = %$name;
5432             } else {
5433 0           $options{ name } = $name;
5434             };
5435              
5436 0 0         if (exists $options{ name }) {
5437 0           my $attr = 'name';
5438 0 0         if ($name =~ s/^\^//) { # if it starts with ^, it's supposed to be a name
    0          
    0          
5439 0           $attr = 'name'
5440             } elsif ($name =~ s/^#//) {
5441 0           $attr = 'id'
5442             } elsif ($name =~ s/^\.//) {
5443 0           $attr = 'class'
5444             };
5445 0           $name = quotemeta($name);
5446 0 0         $value = quotemeta($value) if $value;
5447              
5448 0           _default_limiter( one => \%options );
5449 0           my $q = $self->element_query(
5450             ['input'],
5451             {
5452             $attr => $name,
5453             type => 'checkbox',
5454             maybe value => $value,
5455             }
5456             );
5457 0           $options{ xpath } = $q;
5458             #$options{ xpath } = [
5459             # defined $value
5460             # ? sprintf( q{//input[@type="checkbox" and @%s="%s" and @value="%s"]}, $attr, $name, $value)
5461             # : sprintf( q{//input[@type="checkbox" and @%s="%s"]}, $attr, $name)
5462             #];
5463 0 0         $options{ user_info } = defined $value
5464             ? "Checkbox with name '$name' and value '$value'"
5465             : "Checkbox with name '$name'";
5466             };
5467              
5468 0 0         if ($options{ dom }) {
5469 0           @boxes = $options{ dom };
5470             } else {
5471 0           @boxes = $self->_option_query(%options);
5472             };
5473              
5474 0           my $target = $boxes[0];
5475 0   0       my $is_set = ($target->get_attribute( 'checked', live => 1 ) || '') eq 'checked';
5476 0 0 0       if ($set xor $is_set) {
5477 0 0         if ($set) {
5478 0           $target->set_attribute('checked', 'checked');
5479             } else {
5480 0           $target->set_attribute('checked', undef);
5481             };
5482             };
5483             };
5484              
5485             =head2 C<< $mech->untick( $name, $value ) >>
5486              
5487             $mech->untick('spam_confirm','yes',undef)
5488              
5489             Causes the checkbox to be unticked. Shorthand for
5490              
5491             $mech->tick($name,$value,undef)
5492              
5493             =cut
5494              
5495             sub untick {
5496 0     0 1   my ($self, $name, $value) = @_;
5497 0           $self->tick( $name, $value, undef );
5498             };
5499              
5500             =head2 C<< $mech->submit( $form ) >>
5501              
5502             $mech->submit;
5503              
5504             Submits the form. Note that this does B<not> fire the C<onClick>
5505             event and thus also does not fire eventual Javascript handlers.
5506             Maybe you want to use C<< $mech->click >> instead.
5507              
5508             The default is to submit the current form as returned
5509             by C<< $mech->current_form >>.
5510              
5511             =cut
5512              
5513 0     0 1   sub submit($self,$dom_form = $self->current_form) {
  0            
  0            
  0            
5514 0 0         if ($dom_form) {
5515             # We should prepare for navigation here as well
5516             # The __proto__ invocation is so we can have a HTML form field entry
5517             # named "submit"
5518              
5519             $self->_mightNavigate( sub {
5520 0     0     $self->target->send_message(
5521             'Runtime.callFunctionOn',
5522             objectId => $dom_form->objectId,
5523             functionDeclaration => 'function() { var action = this.action; var isCallable = action && typeof(action) === "function"; if( isCallable) { action() } else { this.__proto__.submit.apply(this) }}'
5524             );
5525             })
5526 0           ->get;
5527              
5528 0           $self->invalidate_cached_values;
5529             } else {
5530 0           croak "I don't know which form to submit, sorry.";
5531             }
5532 0           return $self->response;
5533             };
5534              
5535             =head2 C<< $mech->submit_form( %options ) >>
5536              
5537             $mech->submit_form(
5538             with_fields => {
5539             user => 'me',
5540             pass => 'secret',
5541             }
5542             );
5543              
5544             This method lets you select a form from the previously fetched page,
5545             fill in its fields, and submit it. It combines the form_number/form_name,
5546             C<< ->set_fields >> and C<< ->click methods >> into one higher level call. Its
5547             arguments are a list of key/value pairs, all of which are optional.
5548              
5549             =over 4
5550              
5551             =item *
5552              
5553             C<< form => $mech->current_form() >>
5554              
5555             Specifies the form to be filled and submitted. Defaults to the current form.
5556              
5557             =item *
5558              
5559             C<< fields => \%fields >>
5560              
5561             Specifies the fields to be filled in the current form
5562              
5563             =item *
5564              
5565             C<< with_fields => \%fields >>
5566              
5567             Probably all you need for the common case. It combines a smart form selector
5568             and data setting in one operation. It selects the first form that contains
5569             all fields mentioned in \%fields. This is nice because you don't need to
5570             know the name or number of the form to do this.
5571              
5572             (calls L<< /$mech->form_with_fields() >> and L<< /$mech->set_fields() >>).
5573              
5574             If you choose this, the form_number, form_name, form_id and fields options
5575             will be ignored.
5576              
5577             =back
5578              
5579             =cut
5580              
5581 0     0 1   sub submit_form($self,%options) {;
  0            
  0            
  0            
5582              
5583 0           my $form = delete $options{ form };
5584 0           my $fields;
5585 0 0         if (! $form) {
5586 0 0         if ($fields = delete $options{ with_fields }) {
5587 0           my @names = keys %$fields;
5588 0           $form = $self->form_with_fields( \%options, @names );
5589 0 0         if (! $form) {
5590 0           $self->signal_condition("Couldn't find a matching form for @names.");
5591             return
5592 0           };
5593             } else {
5594 0   0       $fields = delete $options{ fields } || {};
5595 0           $form = $self->current_form;
5596             };
5597             };
5598              
5599 0 0         if (! $form) {
5600 0           $self->signal_condition("No form found to submit.");
5601             return
5602 0           };
5603             #warn Dumper $fields;
5604             #$self->log('debug', sprintf 'Submitting form %s with fields %s', $form->{id}, Dumper $fields);
5605 0           $self->do_set_fields( form => $form, fields => $fields );
5606              
5607 0           my $response;
5608 0 0         if ( $options{button} ) {
5609 0   0       $response = $self->click( $options{button}, $options{x} || 0, $options{y} || 0 );
      0        
5610             }
5611             else {
5612 0           $response = $self->submit();
5613             }
5614 0           return $response;
5615              
5616             }
5617              
5618             =head2 C<< $mech->set_fields( $name => $value, ... ) >>
5619              
5620             $mech->set_fields(
5621             user => 'me',
5622             pass => 'secret',
5623             );
5624              
5625             This method sets multiple fields of the current form. It takes a list of
5626             field name and value pairs. If there is more than one field with the same
5627             name, the first one found is set. If you want to select which of the
5628             duplicate field to set, use a value which is an anonymous array which
5629             has the field value and its number as the 2 elements.
5630              
5631             $mech->set_fields(
5632             user => 'me',
5633             pass => 'secret',
5634             pass => [ 'secret', 2 ], # repeated password field
5635             );
5636              
5637             =cut
5638              
5639 0     0 1   sub set_fields($self, %fields) {;
  0            
  0            
  0            
5640 0           my $f = $self->current_form;
5641 0 0         if (! $f) {
5642 0           croak "Can't set fields: No current form set.";
5643             };
5644 0           $self->do_set_fields(form => $f, fields => \%fields);
5645             };
5646              
5647 0     0 0   sub do_set_fields($self, %options) {
  0            
  0            
  0            
5648 0           my $form = delete $options{ form };
5649 0           my $fields = delete $options{ fields };
5650              
5651 0           while (my($n,$v) = each %$fields) {
5652 0           my $index = undef;
5653 0 0         if (ref $v) {
5654 0           ($v,my $num) = @$v;
5655 0           $index = $num;
5656             };
5657              
5658 0           $self->get_set_value( node => $form, name => $n, value => $v, index => $index, %options );
5659             }
5660             };
5661              
5662             =head1 CONTENT MONITORING METHODS
5663              
5664             =head2 C<< $mech->is_visible( $element ) >>
5665              
5666             =head2 C<< $mech->is_visible( %options ) >>
5667              
5668             if ($mech->is_visible( selector => '#login' )) {
5669             print "You can log in now.";
5670             };
5671              
5672             Returns true if the element is visible, that is, it is
5673             a member of the DOM and neither it nor its ancestors have
5674             a CSS C<visibility> attribute of C<hidden> or
5675             a C<display> attribute of C<none>.
5676              
5677             You can either pass in a DOM element or a set of key/value
5678             pairs to search the document for the element you want.
5679              
5680             =over 4
5681              
5682             =item *
5683              
5684             C<xpath> - the XPath query
5685              
5686             =item *
5687              
5688             C<selector> - the CSS selector
5689              
5690             =item *
5691              
5692             C<dom> - a DOM node
5693              
5694             =back
5695              
5696             The remaining options are passed through to either the
5697             L<< /$mech->xpath|xpath >> or L<< /$mech->selector|selector >> method.
5698              
5699             =cut
5700              
5701 0     0 1   sub is_visible ( $self, @ ) {
  0            
  0            
5702 0           my %options;
5703 0 0         if (2 == @_) {
5704 0           ($self,$options{dom}) = @_;
5705             } else {
5706 0           ($self,%options) = @_;
5707             };
5708 0           _default_limiter( 'maybe', \%options );
5709 0 0         if (! $options{dom}) {
5710 0           $options{dom} = $self->_option_query(%options);
5711             };
5712             # No element means not visible
5713             return
5714 0 0         unless $options{ dom };
5715             #$options{ window } ||= $self->tab->{linkedBrowser}->{contentWindow};
5716              
5717 0           my $id = $options{ dom }->objectId;
5718 0           my ($val, $type) = $self->callFunctionOn(<<'JS', objectId => $id, arguments => []); #->get;
5719             function ()
5720             {
5721             var obj = this;
5722             while (obj) {
5723             // No object
5724             if (!obj) return false;
5725              
5726             try {
5727             if( obj["parentNode"] ) 1;
5728             } catch (e) {
5729             // Dead object
5730             return false
5731             };
5732             // Descends from document, so we're done
5733             if (obj.parentNode === obj.ownerDocument) {
5734             return true;
5735             };
5736             // Not in the DOM
5737             if (!obj.parentNode) {
5738             return false;
5739             };
5740             // Direct style check
5741             if (obj.style) {
5742             if (obj.style.display == 'none') return false;
5743             if (obj.style.visibility == 'hidden') return false;
5744             };
5745              
5746             if (window.getComputedStyle) {
5747             var style = window.getComputedStyle(obj, null);
5748             if (style.display == 'none') {
5749             return false; }
5750             if (style.visibility == 'hidden') {
5751             return false;
5752             };
5753             };
5754             obj = obj.parentNode;
5755             };
5756             // The object does not live in the DOM at all
5757             return false
5758             }
5759             JS
5760 0 0         $type eq 'boolean'
5761             or die "Don't know how to handle Javascript type '$type'";
5762 0           return $val
5763             };
5764              
5765             =head2 C<< $mech->wait_until_invisible( $element ) >>
5766              
5767             =head2 C<< $mech->wait_until_invisible( %options ) >>
5768              
5769             $mech->wait_until_invisible( $please_wait );
5770              
5771             Waits until an element is not visible anymore.
5772              
5773             Takes the same options as L<< $mech->is_visible/->is_visible >>.
5774              
5775             In addition, the following options are accepted:
5776              
5777             =over 4
5778              
5779             =item *
5780              
5781             C<timeout> - the timeout after which the function will C<croak>. To catch
5782             the condition and handle it in your calling program, use an L<eval> block.
5783             A timeout of C<0> means to never time out.
5784              
5785             See also C<max_wait> if you want to wait a limited time for an element to
5786             appear.
5787              
5788             =item *
5789              
5790             C<max_wait> - the maximum time to wait until the function will return.
5791             A max_wait of C<0> means to never time out. If the element is still visible,
5792             the function will return a false value.
5793              
5794             =item *
5795              
5796             C<sleep> - the interval in seconds used to L<sleep>. Subsecond
5797             intervals are possible.
5798              
5799             =back
5800              
5801             Note that when passing in a selector, that selector is requeried
5802             on every poll instance. So the following query will work as expected:
5803              
5804             xpath => '//*[contains(text(),"stand by")]'
5805              
5806             This also means that if your selector query relies on finding
5807             a changing text, you need to pass the node explicitly instead of
5808             passing the selector.
5809              
5810             =cut
5811              
5812 0     0 1   sub wait_until_invisible( $self, %options ) {
  0            
  0            
  0            
5813 0 0         if (2 == @_) {
5814 0           ($self,$options{dom}) = @_;
5815             } else {
5816 0           ($self,%options) = @_;
5817             };
5818 0   0       my $sleep = delete $options{ sleep } || 0.3;
5819 0   0       my $timeout = delete $options{ timeout } || 0;
5820 0   0       my $wait = delete $options{ max_wait } || 0;
5821 0   0       $timeout ||= $wait;
5822              
5823 0           _default_limiter( 'maybe', \%options );
5824              
5825 0           my $timeout_after;
5826 0 0         if ($timeout) {
5827 0           $timeout_after = time + $timeout;
5828             };
5829 0           my $v;
5830             my $node;
5831 0   0       do {
      0        
5832 0           $node = $options{ dom };
5833 0 0         if (! $node) {
5834 0           $node = $self->_option_query(%options);
5835             };
5836             return
5837 0 0         unless $node;
5838 0           $self->sleep( $sleep );
5839              
5840             # If $node goes away due to a page reload, ->is_visible could die:
5841 0           $v = eval { $self->is_visible($node) };
  0            
5842             } while ( $v
5843             and (!$timeout or time < $timeout_after ));
5844 0 0 0       if ($v and $timeout and time >= $timeout_after) {
      0        
5845 0 0         if( $wait ) {
5846             return()
5847 0           } else {
5848 0           croak "Timeout of $timeout seconds reached while waiting for element to become invisible";
5849             };
5850             };
5851 0           return 1;
5852             };
5853              
5854             =head2 C<< $mech->wait_until_visible( %options ) >>
5855              
5856             $mech->wait_until_visible( selector => 'a.download' );
5857              
5858             Waits until an query returns a visible element.
5859              
5860             Takes the same options as L<< $mech->is_visible/->is_visible >>.
5861              
5862             In addition, the following options are accepted:
5863              
5864             =over 4
5865              
5866             =item *
5867              
5868             C<timeout> - the timeout after which the function will C<croak>. To catch
5869             the condition and handle it in your calling program, use an L<eval> block.
5870             A timeout of C<0> means to never time out.
5871              
5872             =item *
5873              
5874             C<sleep> - the interval in seconds used to L<sleep>. Subsecond
5875             intervals are possible.
5876              
5877             =back
5878              
5879             Note that when passing in a selector, that selector is requeried
5880             on every poll instance. So the following query will work as expected:
5881              
5882             xpath => '//*[contains(text(),"click here for download")]'
5883              
5884             =cut
5885              
5886 0     0 1   sub wait_until_visible( $self, %options ) {
  0            
  0            
  0            
5887 0   0       my $sleep = delete $options{ sleep } || 0.3;
5888 0   0       my $timeout = delete $options{ timeout } || 0;
5889              
5890 0           _default_limiter( 'any', \%options );
5891              
5892 0           my $timeout_after;
5893 0 0         if ($timeout) {
5894 0           $timeout_after = time + $timeout;
5895             };
5896 0   0       do {
5897             # If $node goes away due to a page reload, ->is_visible could die:
5898             my @nodes =
5899 0           grep { eval { $self->is_visible( dom => $_ ) } }
  0            
  0            
5900             $self->_option_query(%options);
5901              
5902 0 0         if( @nodes ) {
5903             return @nodes
5904 0           };
5905 0           $self->sleep( $sleep );
5906             } while (!$timeout_after or time < $timeout_after );
5907 0 0         if (time >= $timeout_after) {
5908 0           croak "Timeout of $timeout seconds reached while waiting for element to become invisible";
5909             };
5910             };
5911              
5912             =head1 CONTENT RENDERING METHODS
5913              
5914             =head2 C<< $mech->content_as_png() >>
5915              
5916             my $png_data = $mech->content_as_png();
5917              
5918             # Create scaled-down 480px wide preview
5919             my $png_data = $mech->content_as_png(undef, { width => 480 });
5920              
5921             Returns the given tab or the current page rendered as PNG image.
5922              
5923             All parameters are optional.
5924              
5925             =over 4
5926              
5927             =back
5928              
5929             This method is specific to WWW::Mechanize::Chrome.
5930              
5931             =cut
5932              
5933 0     0     sub _as_raw_png( $self, $image ) {
  0            
  0            
  0            
5934 0           my $data;
5935 0           $image->write( data => \$data, type => 'png' );
5936 0           $data
5937             }
5938              
5939 0     0     sub _content_as_png($self, $rect={}, $target={} ) {
  0            
  0            
  0            
  0            
5940 0     0     $self->target->send_message('Page.captureScreenshot', format => 'png' )->then( sub( $res ) {
  0            
  0            
5941 0           require Imager;
5942 0           my $img = Imager->new ( data => decode_base64( $res->{data} ), format => 'png' );
5943             # Cut out the wanted part
5944 0 0         if( scalar keys %$rect) {
5945 0           $img = $img->crop( %$rect );
5946             };
5947             # Resize image to width/height
5948 0 0         if( scalar keys %$target) {
5949 0           my %args;
5950             $args{ ypixels } = $target->{ height }
5951 0 0         if $target->{height};
5952             $args{ xpixels } = $target->{ width }
5953 0 0         if $target->{width};
5954 0   0       $args{ scalefactor } = $target->{ scalex } || $target->{scaley};
5955 0           $img = $img->scale( %args );
5956             };
5957 0           return Future->done( $img )
5958 0           });
5959             };
5960              
5961              
5962 0     0 1   sub content_as_png($self, $rect={}, $target={}) {
  0            
  0            
  0            
  0            
5963 0           my $img = $self->_content_as_png( $rect, $target )->get;
5964 0           return $self->_as_raw_png( $img );
5965             };
5966              
5967 0     0 0   sub getResourceTree_future( $self ) {
  0            
  0            
5968 0           $self->target->send_message( 'Page.getResourceTree' )
5969 0     0     ->then( sub( $result ) {
  0            
5970             Future->done( $result->{frameTree} )
5971 0           })
5972 0           }
5973              
5974 0     0 0   sub getResourceContent_future( $self, $url_or_resource, $frameId=$self->frameId, %additional ) {
  0            
  0            
  0            
  0            
  0            
5975 0 0         my $url = ref $url_or_resource ? $url_or_resource->{url} : $url_or_resource;
5976 0 0         %additional = (%$url_or_resource,%additional) if ref $url_or_resource;
5977 0           $self->target->send_message( 'Page.getResourceContent', frameId => $frameId, url => $url )
5978 0     0     ->then( sub( $result ) {
  0            
5979 0 0         if( delete $result->{base64Encoded}) {
5980             $result->{content} = decode_base64( $result->{content} )
5981 0           } else {
5982 0           $result->{_utf8} = 1;
5983             };
5984 0           %$result = (%additional, %$result);
5985 0           Future->done( $result )
5986             })
5987 0           }
5988              
5989             # Replace that later with MIME::Detect
5990             our %extensions = (
5991             'image/jpeg' => '.jpg',
5992             'image/png' => '.png',
5993             'image/gif' => '.gif',
5994             'text/html' => '.html',
5995             'text/plain' => '.txt',
5996             'text/stylesheet' => '.css',
5997             'text/javascript' => '.js',
5998             'application/javascript' => '.js',
5999             );
6000              
6001 0     0     sub _saveResourceTree( $self, $tree, $names, $seen, $wanted, $save, $base_dir ) {
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
6002 0           my @requested;
6003             # Also fetch the frame itself?!
6004             # Or better reuse ->content?!
6005             # $tree->{frame}
6006             # build the map from URLs to file names
6007             # This should become a separate method
6008             # Also something like get_page_resources, that returns the linear
6009             # list of resources for all frames etc.
6010             my @wanted;
6011 0           for my $res ($tree->{frame}, @{ $tree->{resources}}) {
  0            
6012 0 0         if( $seen->{ $res->{url} } ) {
6013             #warn "Skipping $res->{url} (already saved)";
6014 0           next;
6015             };
6016 0 0         if( !$wanted->($res) ) {
6017             #warn "Don't want $res->{url}";
6018 0           next;
6019             };
6020             #warn "Do want $res->{url}";
6021              
6022 0           my $target;
6023 0 0         if( exists $names->{ $res->{url}}) {
6024             # User-specified names always take precedence
6025 0           $target = $names->{ $res->{url}};
6026 0           $names->{ $res->{url} } = $target;
6027              
6028             } else {
6029             # find a non-duplicate name
6030 0           $target = $self->filenameFromUrl( $res->{url}, $extensions{ $res->{mimeType} });
6031 0           my %filenames = reverse %$names;
6032              
6033 0           my $duplicates;
6034 0           my $old_target = $target;
6035 0           while( $filenames{ $target }) {
6036 0           $duplicates++;
6037 0           ( $target = $old_target )=~ s!\.(\w+)$!_$duplicates.$1!;
6038             };
6039 0           $names->{ $res->{url} } = File::Spec->catfile( $base_dir, $target );
6040             };
6041              
6042 0           push @wanted, $res;
6043             };
6044              
6045             # retrieve and save the resource content for each resource
6046 0           for my $res (@wanted) {
6047 0           my $fetch = $self->getResourceContent_future( $res );
6048 0 0         if( $save ) {
6049             #warn "Will save $res->{url}";
6050             $fetch = $fetch->then( $save )->else(sub {
6051 0     0     warn "Fetch failed:";
6052 0           warn "@_";
6053 0           });
6054             };
6055 0           push @requested, $fetch;
6056             };
6057              
6058             # recurse through the subframes
6059 0 0         if( my $t = $tree->{childFrames}) {
6060 0           for my $child (@$t) {
6061 0           push @requested, $self->_saveResourceTree( $child, $names, $seen, $wanted, $save, $base_dir );
6062             };
6063             };
6064              
6065             return Future->wait_all( @requested )->catch(sub {
6066 0     0     warn $@;
6067 0           });
6068             }
6069              
6070             # Allow the options to specify whether to filter duplicates here
6071 0     0 0   sub fetchResources_future( $self, %options ) {
  0            
  0            
  0            
6072 0   0       $options{ save } ||= undef;
6073 0   0       $options{ seen } ||= {};
6074 0   0       $options{ names } ||= {};
6075 0   0       $options{ target_dir } ||= '.';
6076 0   0 0     $options{ wanted } ||= sub( $res ) { $res->{url} =~ /^(https?):/i };
  0            
  0            
  0            
  0            
6077 0           my $seen = $options{ seen };
6078 0           my $names = $options{ names };
6079 0           my $wanted = $options{ wanted };
6080 0           my $save = $options{ save };
6081 0           my $base_dir = $options{ target_dir };
6082              
6083 0           my $s = $self;
6084 0           weaken $s;
6085              
6086 0           $self->getResourceTree_future
6087 0     0     ->then( sub( $tree ) {
  0            
6088 0           $s->_saveResourceTree($tree, $names, $seen, $wanted, $save, $base_dir);
6089             })->catch(sub {
6090 0     0     warn @_;
6091 0           });
6092             }
6093              
6094             =head2 C<< $mech->saveResources_future >>
6095              
6096             my $file_map = $mech->saveResources_future(
6097             target_file => 'this_page.html',
6098             target_dir => 'this_page_files/',
6099             wanted => sub { $_[0]->{url} =~ m!^https?:!i },
6100             )->get();
6101              
6102             Rough prototype of "Save Complete Page" feature
6103              
6104             =cut
6105              
6106 0     0 1   sub saveResources_future( $self, %options ) {
  0            
  0            
  0            
6107             my $target_file = $options{ target_file }
6108 0 0         or croak "Need filename to save as ('target_file')";
6109 0           my $target_dir = $options{ target_dir };
6110 0 0         if( ! defined $target_dir ) {
6111 0           ($target_dir = $target_file) =~ s!\.\w+$! files!i;
6112             };
6113 0 0         if( not -e $target_dir ) {
6114 0 0         mkdir $target_dir
6115             or croak "Couldn't create '$target_dir': $!";
6116             }
6117              
6118 0           my %names = (
6119             $self->uri => $target_file,
6120             );
6121 0           my $s = $self;
6122 0           weaken $s;
6123             $self->fetchResources_future(
6124             names => \%names,
6125             seen => \my %seen,
6126             target_dir => $target_dir,
6127             maybe wanted => $options{ wanted },
6128 0     0     save => sub( $resource ) {
  0            
  0            
6129             # For mime/html targets without a name, use the title?!
6130             # Rewrite all HTML, CSS links
6131              
6132             # We want to store the top HTML under the name passed in (!)
6133 0   0       $names{ $resource->{url} } ||= File::Spec->catfile( $target_dir, $names{ $resource->{url} });
6134             my $target = $names{ $resource->{url} }
6135 0 0         or die "Don't have a filename for URL '$resource->{url}' ?!";
6136 0           $s->log( 'debug', "Saving '$resource->{url}' to '$target'" );
6137 0 0         open my $fh, '>', $target
6138             or croak "Couldn't save url '$resource->{url}' to $target: $!";
6139 0 0         if( $resource->{_utf8}) {
6140 0           binmode $fh, ':encoding(UTF-8)';
6141             } else {
6142 0           binmode $fh;
6143             };
6144              
6145 0           print $fh $resource->{content};
6146 0           CORE::close( $fh );
6147              
6148 0           Future->done( $resource );
6149             },
6150 0     0     )->then( sub( @resources ) {
  0            
  0            
6151 0           Future->done( \%names );
6152             })->catch(sub {
6153 0     0     warn $@;
6154 0           });
6155             }
6156              
6157 0     0 0   sub filenameFromUrl( $self, $url, $extension ) {
  0            
  0            
  0            
  0            
6158 0           my $target = URI->new( $url )->path;
6159              
6160 0           $target =~ s![\&\?\<\>\{\}\|\:\*]!_!g;
6161 0           $target =~ s!.*[/\\]!!;
6162              
6163             # Add/change extension here
6164              
6165 0           return $target
6166             }
6167              
6168             =head2 C<< $mech->viewport_size >>
6169              
6170             print Dumper $mech->viewport_size;
6171             $mech->viewport_size({ width => 1388, height => 792 });
6172              
6173             Returns (or sets) the new size of the viewport (the "window").
6174              
6175             The recognized keys are:
6176              
6177             width
6178             height
6179             deviceScaleFactor
6180             mobile
6181             screenWidth
6182             screenHeight
6183             positionX
6184             positionY
6185              
6186             =cut
6187              
6188 0     0 0   sub viewport_size_future( $self, $new={} ) {
  0            
  0            
  0            
6189 0           my $params = dclone $new;
6190 0 0         if( keys %$params) {
6191 0           my %reset = (
6192             mobile => JSON::false,
6193             width => 0,
6194             height => 0,
6195             deviceScaleFactor => 0,
6196             scale => 1,
6197             screenWidth => 0,
6198             screenHeight => 0,
6199             positionX => 0,
6200             positionY => 0,
6201             dontSetVisibleSize => JSON::false,
6202             screenOrientation => {
6203             type => 'landscapePrimary',
6204             angle => 0,
6205             },
6206             #viewport => {
6207             # 'x' => 0,
6208             # 'y' => 0,
6209             # width => 0,
6210             # height => 0,
6211             # scale => 1,
6212             #}
6213             );
6214 0           for my $field (qw( mobile width height deviceScaleFactor )) {
6215 0 0         if( ! exists $params->{ $field }) {
6216 0           $params->{$field} = $reset{ $field };
6217             };
6218             };
6219 0           return $self->target->send_message('Emulation.setDeviceMetricsOverride', %$params );
6220             } else {
6221 0           return $self->target->send_message('Emulation.clearDeviceMetricsOverride' );
6222             };
6223             };
6224              
6225 0     0 1   sub viewport_size( $self, $new={} ) {
  0            
  0            
  0            
6226 0           $self->viewport_size_future($new)->get
6227             };
6228              
6229             =head2 C<< $mech->element_as_png( $element ) >>
6230              
6231             my $shiny = $mech->selector('#shiny', single => 1);
6232             my $i_want_this = $mech->element_as_png($shiny);
6233              
6234             Returns PNG image data for a single element
6235              
6236             =cut
6237              
6238             sub element_as_png {
6239 0     0 1   my ($self, $element) = @_;
6240              
6241 0           $self->render_element( element => $element, format => 'png' )
6242             };
6243              
6244             =head2 C<< $mech->render_element( %options ) >>
6245              
6246             my $shiny = $mech->selector('#shiny', single => 1);
6247             my $i_want_this= $mech->render_element(
6248             element => $shiny,
6249             format => 'png',
6250             );
6251              
6252             Returns the data for a single element
6253             or writes it to a file. It accepts
6254             all options of C<< ->render_content >>.
6255              
6256             Note that while the image will have the node in the upper left
6257             corner, the width and height of the resulting image will still
6258             be the size of the browser window. Cut the image using
6259             C<< element_coordinates >> if you need exactly the element.
6260              
6261             =cut
6262              
6263             sub render_element {
6264 0     0 1   my ($self, %options) = @_;
6265             my $element= delete $options{ element }
6266 0 0         or croak "No element given to render.";
6267              
6268 0           my $cliprect = $self->element_coordinates( $element );
6269             my $res = Future->wait_all(
6270             #$self->target->send_message('Emulation.setVisibleSize', width => int $cliprect->{width}, height => int $cliprect->{height} ),
6271             $self->target->send_message(
6272             'Emulation.forceViewport',
6273             'y' => int $cliprect->{top},
6274             'x' => int $cliprect->{left},
6275             scale => 1.0
6276             ),
6277             )->then(sub {
6278 0           $self->_content_as_png()->then( sub( $img ) {
6279             my $element = $img->crop(
6280             left => 0,
6281             top => 0,
6282             width => $cliprect->{width},
6283 0           height => $cliprect->{height});
6284 0           Future->done( $self->_as_raw_png( $element ));
6285             })
6286 0     0     })->get;
  0            
6287              
6288 0           Future->wait_all(
6289             #$self->target->send_message('Emulation.setVisibleSize', width => $cliprect->{width}, height => $cliprect->{height} ),
6290             $self->target->send_message('Emulation.resetViewport'),
6291             )->get;
6292              
6293 0           $res
6294             };
6295              
6296             =head2 C<< $mech->element_coordinates( $element ) >>
6297              
6298             my $shiny = $mech->selector('#shiny', single => 1);
6299             my ($pos) = $mech->element_coordinates($shiny);
6300             print $pos->{left},',', $pos->{top};
6301              
6302             Returns the page-coordinates of the C<$element>
6303             in pixels as a hash with four entries, C<left>, C<top>, C<width> and C<height>.
6304              
6305             This function might get moved into another module more geared
6306             towards rendering HTML.
6307              
6308             =cut
6309              
6310             sub element_coordinates {
6311 0     0 1   my ($self, $element) = @_;
6312 0           my $cliprect = $self->target->send_message('Runtime.callFunctionOn', objectId => $element->objectId, functionDeclaration => <<'JS', arguments => [], returnByValue => JSON::true)->get->{result}->{value};
6313             function() {
6314             var r = this.getBoundingClientRect();
6315             return {
6316             top : r.top
6317             , left: r.left
6318             , width: r.width
6319             , height: r.height
6320             }
6321             }
6322             JS
6323             };
6324              
6325             =head2 C<< $mech->render_content(%options) >>
6326              
6327             my $pdf_data = $mech->render_content( format => 'pdf' );
6328              
6329             Returns the current page rendered as PDF or PNG
6330             as a bytestring.
6331              
6332             Note that the PDF format will only be successful with headless Chrome. At least
6333             on Windows, when launching Chrome with a UI, printing to PDF will
6334             be unavailable.
6335              
6336             This method is specific to WWW::Mechanize::Chrome.
6337              
6338             =cut
6339              
6340 0     0 1   sub render_content( $self, %options ) {
  0            
  0            
  0            
6341 0   0       $options{ format } ||= 'png';
6342              
6343 0           my $fmt = delete $options{ format };
6344 0           my $filename = delete $options{ filename };
6345              
6346 0           my $payload;
6347 0 0         if( $fmt eq 'png' ) {
    0          
6348 0           $payload = $self->content_as_png( %options )
6349             } elsif( $fmt eq 'pdf' ) {
6350 0           $payload = $self->content_as_pdf( %options );
6351             };
6352              
6353 0 0         if( defined $filename ) {
6354 0 0         open my $fh, '>:raw', $filename
6355             or croak "Couldn't create '$filename': $!";
6356 0           print {$fh} $payload;
  0            
6357             };
6358              
6359 0           $payload
6360             }
6361              
6362             =head2 C<< $mech->content_as_pdf(%options) >>
6363              
6364             my $pdf_data = $mech->content_as_pdf();
6365              
6366             my $pdf_data = $mech->content_as_pdf( format => 'A4' );
6367              
6368             my $pdf_data = $mech->content_as_pdf( paperWidth => 8, paperHeight => 11 );
6369              
6370             Returns the current page rendered in PDF format as a bytestring. The page format
6371             can be specified through the C<format> option.
6372              
6373             Note that this method will only be successful with headless Chrome. At least on
6374             Windows, when launching Chrome with a UI, printing to PDF will be unavailable.
6375             See the C<html-to-pdf.pl> script in the C<examples/> directory of this distribution.
6376              
6377             This method is specific to WWW::Mechanize::Chrome.
6378              
6379             =cut
6380              
6381             our %PaperFormats = (
6382             letter => {width => 8.5, height => 11 },
6383             legal => {width => 8.5, height => 14 },
6384             tabloid => {width => 11, height => 17 },
6385             ledger => {width => 17, height => 11 },
6386             a0 => {width => 33.1, height => 46.8 },
6387             a1 => {width => 23.4, height => 33.1 },
6388             a2 => {width => 16.5, height => 23.4 },
6389             a3 => {width => 11.7, height => 16.5 },
6390             a4 => {width => 8.27, height => 11.7 },
6391             a5 => {width => 5.83, height => 8.27 },
6392             a6 => {width => 4.13, height => 5.83 },
6393             );
6394              
6395 0     0 1   sub content_as_pdf($self, %options) {
  0            
  0            
  0            
6396 0 0         if( my $format = delete $options{ format }) {
6397 0 0         my $wh = $PaperFormats{ lc $format }
6398             or croak "Unknown paper format '$format'";
6399 0           @options{'paperWidth','paperHeight'} = @{$wh}{'width','height'};
  0            
6400             };
6401              
6402 0           my $base64 = $self->target->send_message('Page.printToPDF', %options)->get->{data};
6403 0           my $payload = decode_base64( $base64 );
6404 0 0         if( my $filename = delete $options{ filename } ) {
6405 0 0         open my $fh, '>:raw', $filename
6406             or croak "Couldn't create '$filename': $!";
6407 0           print {$fh} $payload;
  0            
6408             };
6409 0           return $payload;
6410             };
6411              
6412             =head1 INTERNAL METHODS
6413              
6414             These are methods that are available but exist mostly as internal
6415             helper methods. Use of these is discouraged.
6416              
6417             =head2 C<< $mech->element_query( \@elements, \%attributes ) >>
6418              
6419             my $query = $mech->element_query(['input', 'select', 'textarea'],
6420             { name => 'foo' });
6421              
6422             Returns the XPath query that searches for all elements with C<tagName>s
6423             in C<@elements> having the attributes C<%attributes>. The C<@elements>
6424             will form an C<or> condition, while the attributes will form an C<and>
6425             condition.
6426              
6427             =cut
6428              
6429             sub element_query {
6430 0     0 1   my ($self, $elements, $attributes) = @_;
6431             my $query =
6432             './/*[(' .
6433             join( ' or ',
6434             map {
6435 0           sprintf qq{local-name(.)="%s"}, lc $_
6436             } @$elements
6437             )
6438             . ') and '
6439             . join( " and ",
6440 0           map { sprintf q{@%s="%s"}, $_, $attributes->{$_} }
  0            
6441             sort keys(%$attributes)
6442             )
6443             . ']';
6444             };
6445              
6446             sub post_process
6447             {
6448 0     0 0   my $self = shift;
6449 0 0         if ( $self->{report_js_errors} ) {
6450 0 0         if ( my @errors = $self->js_errors ) {
6451 0           $self->report_js_errors(@errors);
6452 0           $self->clear_js_errors;
6453             }
6454             }
6455             }
6456              
6457             sub report_js_errors
6458             {
6459 0     0 1   my ( $self, @errors ) = @_;
6460             @errors = map {
6461 0           $_->{message} .
6462 0           ( @{$_->{trace}} ? " at $_->{trace}->[-1]->{file} line $_->{trace}->[-1]->{line}" : '') .
6463 0 0 0       ( @{$_->{trace}} && $_->{trace}->[-1]->{function} ? " in function $_->{trace}->[-1]->{function}" : '')
    0          
6464             } @errors;
6465 0 0         Carp::carp("javascript error: @errors") if @errors;
6466             }
6467              
6468             =head1 DEBUGGING METHODS
6469              
6470             This module can collect the screencasts that Chrome can produce. The screencasts
6471             are sent to your callback which either feeds them to C<ffmpeg> to create a video
6472             out of them or dumps them to disk as sequential images.
6473              
6474             sub saveFrame {
6475             my( $mech, $framePNG ) = @_;
6476             print $framePNG->{data};
6477              
6478             }
6479              
6480             $mech->setScreenFrameCallback( \&saveFrame );
6481             ... do stuff ...
6482             $mech->setScreenFrameCallback( undef ); # stop recording
6483              
6484             If you want a premade screencast receiver for debugging headless Chrome
6485             sessions, see L<Mojolicious::Plugin::PNGCast>.
6486              
6487             =cut
6488              
6489 0     0     sub _handleScreencastFrame( $self, $frame ) {
  0            
  0            
  0            
6490             # Meh, this one doesn't get a response I guess. So, not ->send_message, just
6491             # send a JSON packet to acknowledge the frame
6492 0           my $s = $self;
6493 0           weaken $s;
6494             $self->target->send_message(
6495             'Page.screencastFrameAck',
6496             sessionId => 0+$frame->{params}->{sessionId} )->then(sub {
6497 0     0     $s->log('trace', 'Screencast frame acknowledged');
6498 0           $frame->{params}->{data} = decode_base64( $frame->{params}->{data} );
6499 0           $s->{ screenFrameCallback }->( $s, $frame->{params} );
6500 0           Future->done();
6501 0           })->retain;
6502             }
6503              
6504 0     0 0   sub setScreenFrameCallback( $self, $callback=undef, %options ) {
  0            
  0            
  0            
  0            
6505 0           $self->{ screenFrameCallback } = $callback;
6506              
6507 0   0       $options{ format } ||= 'png';
6508 0   0       $options{ everyNthFrame } ||= 1;
6509              
6510 0           my $action;
6511 0           my $s = $self;
6512 0           weaken $s;
6513 0 0         if( $callback ) {
6514 0     0     $self->{ screenFrameCallbackCollector } = sub( $frame ) {
  0            
  0            
6515 0           $s->_handleScreencastFrame( $frame );
6516 0           };
6517             $self->{ screenCastFrameListener } =
6518 0           $self->add_listener('Page.screencastFrame', $self->{ screenFrameCallbackCollector });
6519             $action = $s->target->send_message(
6520             'Page.startScreencast',
6521             format => $options{ format },
6522             everyNthFrame => 0+$options{ everyNthFrame }
6523 0           );
6524             } else {
6525             $action = $self->target->send_message('Page.stopScreencast')->then( sub {
6526             # well, actually, we should only reset this after we're sure that
6527             # the last frame has been processed. Maybe we should send ourselves
6528             # a fake event for that, or maybe Chrome tells us
6529 0     0     delete $s->{ screenCastFrameListener };
6530 0           Future->done(1);
6531 0           });
6532             }
6533 0           $action->get
6534             }
6535              
6536             =head2 C<< $mech->sleep >>
6537              
6538             $mech->sleep( 2 ); # wait for things to settle down
6539              
6540             Suspends the progress of the program while still handling messages from
6541             Chrome.
6542              
6543             The main use of this method is to give Chrome enough time to send all its
6544             screencast frames and to catch up before shutting down the connection.
6545              
6546             =cut
6547              
6548 0     0 0   sub sleep_future( $self, $seconds ) {
  0            
  0            
  0            
6549 0           $self->target->sleep( $seconds );
6550             }
6551              
6552 0     0 1   sub sleep( $self, $seconds ) {
  0            
  0            
  0            
6553 0           $self->sleep_future( $seconds )->get;
6554             }
6555              
6556             1;
6557              
6558             =head1 INCOMPATIBILITIES WITH WWW::Mechanize
6559              
6560             As this module is in a very early stage of development,
6561             there are many incompatibilities. The main thing is
6562             that only the most needed WWW::Mechanize methods
6563             have been implemented by me so far.
6564              
6565             =head2 Unsupported Methods
6566              
6567             At least the following methods are unsupported:
6568              
6569             =over 4
6570              
6571             =item *
6572              
6573             C<< ->find_all_inputs >>
6574              
6575             This function is likely best implemented through C<< $mech->selector >>.
6576              
6577             =item *
6578              
6579             C<< ->find_all_submits >>
6580              
6581             This function is likely best implemented through C<< $mech->selector >>.
6582              
6583             =item *
6584              
6585             C<< ->images >>
6586              
6587             This function is likely best implemented through C<< $mech->selector >>.
6588              
6589             =item *
6590              
6591             C<< ->find_image >>
6592              
6593             This function is likely best implemented through C<< $mech->selector >>.
6594              
6595             =item *
6596              
6597             C<< ->find_all_images >>
6598              
6599             This function is likely best implemented through C<< $mech->selector >>.
6600              
6601             =back
6602              
6603             =head2 Functions that will likely never be implemented
6604              
6605             These functions are unlikely to be implemented because
6606             they make little sense in the context of Chrome.
6607              
6608             =over 4
6609              
6610             =item *
6611              
6612             C<< ->clone >>
6613              
6614             =item *
6615              
6616             C<< ->credentials( $username, $password ) >>
6617              
6618             =item *
6619              
6620             C<< ->get_basic_credentials( $realm, $uri, $isproxy ) >>
6621              
6622             =item *
6623              
6624             C<< ->clear_credentials() >>
6625              
6626             =item *
6627              
6628             C<< ->put >>
6629              
6630             I have no use for it
6631              
6632             =item *
6633              
6634             C<< ->post >>
6635              
6636             This module does not yet support POST requests
6637              
6638             =back
6639              
6640             =head1 INSTALLING
6641              
6642             See L<WWW::Mechanize::Chrome::Install>
6643              
6644             =head1 SEE ALSO
6645              
6646             =over 4
6647              
6648             =item *
6649              
6650             L<https://developer.chrome.com/devtools/docs/debugging-clients> - the Chrome
6651             DevTools homepage
6652              
6653             =item *
6654              
6655             L<https://github.com/GoogleChrome/lighthouse> - Google Lighthouse, the main
6656             client of the Chrome API
6657              
6658             =item *
6659              
6660             L<WWW::Mechanize> - the module whose API grandfathered this module
6661              
6662             =item *
6663              
6664             L<WWW::Mechanize::Chrome::Node> - objects representing HTML in Chrome
6665              
6666             =item *
6667              
6668             L<WWW::Mechanize::Firefox> - a similar module with a visible application
6669             automating Firefox , currently on hiatus, since Mozilla does not yet
6670             implement the Chrome DevTools Protocol properly
6671              
6672             =item *
6673              
6674             L<WWW::Mechanize::PhantomJS> - a similar module without a visible application
6675             automating PhantomJS , now discontinued since PhantomJS is discontinued
6676              
6677             =back
6678              
6679             =head1 MASQUERADING AS OTHER BROWSERS
6680              
6681             Some articles about what you need to change to appear as a different
6682             browser
6683              
6684             L<https://multilogin.com/why-mimicking-a-device-is-almost-impossible/>
6685              
6686             L<https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth>
6687              
6688             =head1 REPOSITORY
6689              
6690             The public repository of this module is
6691             L<https://github.com/Corion/www-mechanize-chrome>.
6692              
6693             =head1 SUPPORT
6694              
6695             The public support forum of this module is L<https://perlmonks.org/>.
6696              
6697             =head1 TALKS
6698              
6699             I've given a German talk at GPW 2017, see L<http://act.yapc.eu/gpw2017/talk/7027>
6700             and L<https://corion.net/talks> for the slides.
6701              
6702             At The Perl Conference 2017 in Amsterdam, I also presented a talk, see
6703             L<http://act.perlconference.org/tpc-2017-amsterdam/talk/7022>.
6704             The slides for the English presentation at TPCiA 2017 are at
6705             L<https://corion.net/talks/WWW-Mechanize-Chrome/www-mechanize-chrome.en.html>.
6706              
6707             At the London Perl Workshop 2017 in London, I also presented a talk, see
6708             L<Youtube|https://www.youtube.com/watch?v=V3WeO-iVkAc> . The slides for
6709             that talk are
6710             L<here|https://corion.net/talks/WWW-Mechanize-Chrome/www-mechanize-chrome.en.html>.
6711              
6712             =head1 BUG TRACKER
6713              
6714             Please report bugs in this module via the Github bug queue at
6715             L<https://github.com/Corion/WWW-Mechanize-Chrome/issues>
6716              
6717             =head1 CONTRIBUTING
6718              
6719             Please see L<WWW::Mechanize::Chrome::Contributing>.
6720              
6721             =head1 KNOWN ISSUES
6722              
6723             Please see L<WWW::Mechanize::Chrome::Troubleshooting>.
6724              
6725             =head1 AUTHOR
6726              
6727             Max Maischein C<corion@cpan.org>
6728              
6729             =head1 CONTRIBUTORS
6730              
6731             Andreas König C<andk@cpan.org>
6732              
6733             Tobias Leich C<froggs@cpan.org>
6734              
6735             Steven Dondley C<s@dondley.org>
6736              
6737             Joshua Pollack
6738              
6739             =head1 COPYRIGHT (c)
6740              
6741             Copyright 2010-2023 by Max Maischein C<corion@cpan.org>.
6742              
6743             =head1 LICENSE
6744              
6745             This module is released under the same terms as Perl itself.
6746              
6747             =cut