File Coverage

blib/lib/WWW/Mechanize/Chrome/Node.pm
Criterion Covered Total %
statement 26 277 9.3
branch 0 32 0.0
condition 0 17 0.0
subroutine 9 46 19.5
pod 6 9 66.6
total 41 381 10.7


line stmt bran cond sub pod time code
1             package WWW::Mechanize::Chrome::Node;
2 68     68   471 use strict;
  68         141  
  68         1888  
3 68     68   1219 use 5.016; # __SUB__
  68         3064  
4 68     68   368 use Moo 2;
  68         1192  
  68         424  
5 68     68   23098 use Filter::signatures;
  68         158  
  68         473  
6 68     68   2354 no warnings 'experimental::signatures';
  68         138  
  68         2870  
7 68     68   469 use feature 'signatures';
  68         183  
  68         7079  
8 68     68   474 use Carp qw( croak );
  68         133  
  68         3476  
9 68     68   433 use JSON;
  68         190  
  68         482  
10              
11 68     68   7667 use Scalar::Util 'weaken';
  68         151  
  68         216827  
12              
13             =head1 NAME
14              
15             WWW::Mechanize::Chrome::Node - represent a Chrome HTML node in Perl
16              
17             =head1 SYNOPSIS
18              
19             (my $node) = $mech->selector('.download');
20             print $node->get_attribute('class'); # "download"
21              
22             =cut
23              
24             our $VERSION = '0.69';
25              
26             =head1 MEMBERS
27              
28             =head2 C<attributes>
29              
30             The attributes this node has
31              
32             =cut
33              
34             has 'attributes' => (
35             is => 'lazy',
36             default => sub { {} },
37             );
38              
39             =head2 C<nodeName>
40              
41             The (tag) name of this node, with a namespace
42              
43             =cut
44              
45             has 'nodeName' => (
46             is => 'ro',
47             );
48              
49             =head2 C<nodeId>
50              
51             The nodeId of this node
52              
53             =cut
54              
55             has 'nodeId' => (
56             is => 'ro',
57             );
58             =head2 C<localName>
59              
60             The local (tag) name of this node
61              
62             =cut
63              
64             has 'localName' => (
65             is => 'ro',
66             );
67              
68             =head2 C<backendNodeId>
69              
70             The id of this node within Chrome
71              
72             =cut
73              
74             has 'backendNodeId' => (
75             is => 'ro',
76             );
77              
78             =head2 C<cachedNodeId>
79              
80             The cached id of this node for this session
81              
82             =cut
83              
84             has 'cachedNodeId' => (
85             is => 'rw',
86             );
87              
88             =head2 C<objectId>
89              
90             Another id of this node within Chrome
91              
92             =cut
93              
94             has 'objectId' => (
95             is => 'lazy',
96             default => sub { $_[0]->_fetchObjectId()->get },
97             );
98              
99             =head2 C<driver>
100              
101             The L<Chrome::DevToolsProtocol::Transport> instance used to communicate
102             with Chrome
103              
104             =cut
105              
106             has 'driver' => (
107             is => 'ro',
108             );
109              
110             # The generation from when our ->nodeId was valid
111             has '_generation' => (
112             is => 'rw',
113             );
114              
115             =head2 C<mech>
116              
117             A weak reference to the L<WWW::Mechanize::Chrome> instance used to communicate
118             with Chrome.
119              
120             =cut
121              
122             has 'mech' => (
123             is => 'ro',
124             weak_ref => 1,
125             );
126              
127             =head1 CONSTRUCTORS
128              
129              
130             =head2 C<< fetchNode >>
131              
132             WWW::Mechanize::Chrome->fetchNode(
133             nodeId => $nodeId,
134             driver => $mech->driver,
135             )->get()
136              
137             Returns a L<Future> that returns a populated node.
138              
139             =cut
140              
141 0     0 1   sub fetchNode( $class, %options ) {
  0            
  0            
  0            
142             my $driver = delete $options{ driver }
143 0 0         or croak "Need a valid driver for communication";
144 0           weaken $driver;
145             defined(my $nodeId = delete $options{ nodeId })
146 0 0         or croak "Need a valid nodeId for requesting";
147 0           my $body = delete $options{ body };
148 0           my $attributes = delete $options{ attributes };
149              
150 0 0         if( $body ) {
151 0           $body = Future->done( $body );
152             } else {
153 0           my %info;
154 0           $body = $driver->send_message( 'DOM.resolveNode', nodeId => 0+$nodeId )
155 0     0     ->then( sub( $info ) {
  0            
156 0           %info = %{$info->{object}};
  0            
157             $driver->send_message( 'DOM.requestNode', objectId => $info{objectId} )
158 0     0     })->then(sub( $info ) {
  0            
  0            
  0            
159 0           %info = (%info, %$info);
160             $driver->send_message( 'DOM.describeNode', objectId => $info{objectId} )
161 0     0     })->then(sub( $info ) {
  0            
  0            
  0            
162 0           %info = (%info, %{$info->{node}}, nodeId => 0+$nodeId);
  0            
163              
164 0           Future->done( \%info );
165 0           });
166             };
167 0 0         if( $attributes ) {
168 0           $attributes = Future->done( $attributes )
169             } else {
170 0           $attributes = $driver->send_message( 'DOM.getAttributes', nodeId => 0+$nodeId );
171             };
172              
173 0     0     return Future->wait_all( $body, $attributes )->then( sub( $body, $attributes ) {
  0            
  0            
  0            
174 0           $body = $body->get;
175 0           my $attr = $attributes->get;
176 0           $attributes = $attr->{attributes};
177 0           my $nodeName = $body->{description};
178 0           $nodeName =~ s!#.*!!;
179             #warn "Backend for $nodeId is $attr->{ backendNodeId }";
180             #use Data::Dumper;
181             #warn Dumper $attr;
182             #warn Dumper $body;
183             #die unless $attr->{backendNodeId};
184             my $node = {
185             cachedNodeId => $nodeId,
186             objectId => $body->{ objectId },
187             backendNodeId => $body->{backendNodeId} || $attr->{ backendNodeId },
188             nodeId => $nodeId,
189             parentId => $body->{ parentId },
190             attributes => {
191 0   0       @{ $attributes },
  0            
192             },
193             nodeName => $nodeName,
194             #driver => $driver,
195             #mech => $s,
196             #_generation => $s->_generation,
197             };
198 0           $node->{driver} = $driver;
199 0           my $n = $class->new( $node );
200              
201             # Fetch additional data into the object
202             #return $n->_nodeId()->then(sub {
203             # unless( $n->backendNodeId ) {
204             # warn Dumper [ $body, $attributes ];
205             # die;
206             # };
207             #});
208 0           Future->done( $n );
209             })->catch(sub {
210 0     0     warn "@_";
211 0           warn "Node $nodeId has gone away in the meantime, could not resolve";
212 0           Future->done( $class->new( {} ) );
213 0           });
214             }
215              
216 0     0     sub _fetchObjectId( $self ) {
  0            
  0            
217             #warn "Realizing objectId";
218 0 0         if( $self->{objectId}) {
219             return Future->done( $self->{objectId} )
220 0           } else {
221 0           weaken(my $s=$self);
222 0     0     $self->driver->send_message('DOM.resolveNode', nodeId => 0+$self->nodeId)->then(sub( $obj ) {
  0            
  0            
223 0           $s->{objectId} = $obj->{object}->{objectId};
224 0           Future->done( $obj->{object}->{objectId} );
225 0           });
226             }
227             }
228              
229 0     0     sub _fetchNodeId($self) {
  0            
  0            
230 0           weaken( my $s = $self );
231 0     0     $self->_fetchObjectId->then(sub( $objectId ) {
  0            
  0            
232 0           $self->driver->send_message('DOM.requestNode', objectId => $objectId)
233 0     0     })->then(sub($d) {
  0            
  0            
234 0 0         if( ! exists $d->{node} ) {
235             # Ugh - that node has gone away before we could request it ...
236 0           Future->done( $d->{nodeId} );
237             } else {
238 0           $s->{backendNodeId} = 0+$d->{node}->{backendNodeId};
239 0   0       $s->{nodeId} = 0+$d->{node}->{nodeId} // 0+$s->{nodeId}; # keep old one ...
240 0   0       $s->cachedNodeId( 0+$d->{node}->{nodeId} // 0+$s->{nodeId} );
241 0           Future->done( $s->{nodeId} );
242             };
243 0           });
244             }
245              
246 0     0     sub _nodeId($self) {
  0            
  0            
247 0           my $nid;
248 0 0         if( my $mech = $self->mech ) {
249 0           my $generation = $mech->_generation;
250 0 0 0       if( !$self->_generation or $self->_generation != $generation ) {
251             # Re-resolve, and hopefully we still have our objectId
252 0           $nid = $self->_fetchNodeId();
253 0           $self->_generation( $generation );
254             }
255             }
256             else {
257 0           $nid = Future->done( 0+$self->cachedNodeId );
258             }
259 0           $nid;
260             }
261             #
262             #=head2 C<< ->nodeId >>
263             #
264             # print $node->nodeId();
265             #
266             #Lazily fetches the node id of this node. Use C<< ->_nodeId >> for a version
267             #that returns a Future.
268             #
269             #=cut
270             #
271             #sub nodeId($self) {
272             # $self->_nodeId()->get;
273             #}
274              
275             =head1 METHODS
276              
277             =cut
278              
279             =head2 C<< ->get_attribute >>
280              
281             print $node->get_attribute('outerHTML');
282              
283             Fetches the attribute of the node from Chrome
284              
285             print $node->get_attribute('href', live => 1);
286              
287             Force a live query of the attribute to Chrome. If the attribute was declared
288             on the node, this overrides the stored value and queries Chrome again for
289             the current value of the attribute.
290              
291             =cut
292              
293 0     0     sub _false_to_undef( $val ) {
  0            
  0            
294 0 0 0       if( ref $val and ref $val eq 'JSON::PP::Boolean' ) {
295 0 0         $val = $val ? $val : undef;
296             }
297 0           return $val
298             }
299              
300 0     0     sub _fetch_attribute_eval( $self, $attribute ) {
  0            
  0            
  0            
301 0           weaken(my $s=$self);
302 0           $self->_fetchObjectId
303 0     0     ->then( sub( $objectId ) {
  0            
304 0           $s->driver->send_message('Runtime.callFunctionOn',
305             functionDeclaration => '(o,a) => { console.log(o[a]); return o[a] }',
306             arguments => [ { objectId => $objectId }, { value => $attribute } ],
307             objectId => $objectId,
308             returnByValue => JSON::true
309             )
310             })
311 0     0     ->then(sub($res) {
  0            
  0            
312 0           $res = $res->{result}->{value};
313 0           return Future->done( _false_to_undef( $res ))
314 0           });
315             }
316              
317 0     0     sub _fetch_attribute_attribute( $self, $attribute ) {
  0            
  0            
  0            
318 0           $self->driver->send_message('DOM.getAttributes',
319             nodeId => 0+$self->nodeId,
320             )
321 0     0     ->then(sub($_res) {
  0            
322 0           my %attr = $_res->{attributes}->@*;
323 0           my $res = $attr{ $attribute };
324 0           return Future->done( _false_to_undef( $res ))
325 0           });
326             }
327              
328 0     0     sub _fetch_attribute_property( $self, $attribute ) {
  0            
  0            
  0            
329 0           $self->_fetchObjectId
330 0     0     ->then( sub( $objectId ) {
  0            
331 0           $self->driver->send_message('Runtime.getProperties',
332             objectId => $objectId,
333             #ownProperties => JSON::true,
334             #accessorPropertiesOnly => JSON::true,
335             #returnByValue => JSON::true
336             )})
337 0     0     ->then(sub($_res) {
  0            
  0            
338 0           (my $attr) = grep { $_->{name} eq $attribute } $_res->{result}->@*;
  0            
339 0   0       $attr //= {};
340 0           my $res = $attr->{value}->{value};
341 0           return Future->done( _false_to_undef( $res ))
342 0           });
343             }
344              
345 0     0     sub _fetch_attribute( $self, $attribute ) {
  0            
  0            
  0            
346 0           weaken(my $s=$self);
347 0     0     my $attr = $s->_fetch_attribute_attribute( $attribute )->then(sub ($val) {
  0            
  0            
348 0 0         if( ! defined $val) {
349 0           my $attr = $s->_fetch_attribute_property( $attribute )->then(sub ($val) {
350 0 0         if( ! defined $val) {
351 0           return $s->_fetch_attribute_eval( $attribute )
352             } else {
353 0           return Future->done( $val )
354             }
355             })
356 0           } else {
357 0           return Future->done( $val )
358             }
359 0           });
360             }
361              
362 0     0 0   sub get_attribute_future( $self, $attribute, %options ) {
  0            
  0            
  0            
  0            
363 0           my $s = $self;
364 0           weaken $s;
365              
366 0 0         if( $attribute eq 'innerHTML' ) {
    0          
367 0           my $html = $s->get_attribute_future('outerHTML')
368 0     0     ->then(sub( $html ) {
  0            
369             # Strip first and last tag in a not so elegant way
370 0           $html =~ s!\A<[^>]+>!!;
371 0           $html =~ s!<[^>]+>\z!!;
372 0           Future->done( $html )
373 0           });
374 0           return $html
375              
376             } elsif( $attribute eq 'outerHTML' ) {
377 0           my $nid = $s->_fetchNodeId();
378             # If we only have a backendNodeId, use that
379 0     0     my $html = $nid->then(sub( $nodeId ) {
  0            
  0            
380 0           (my $key) = grep { $s->$_ } (qw(backendNodeId nodeId));
  0            
381 0           my $val;
382              
383 0 0         if( ! $key ) {
384 0           $key = 'nodeId';
385 0           $val = 0+$nodeId;
386             } else {
387 0           $val = $self->$key;
388             };
389              
390             #$s->driver->send_message('DOM.getOuterHTML', nodeId => 0+$nodeId )
391 0           $s->driver->send_message('DOM.getOuterHTML', $key => $val )
392 0     0     })->then(sub( $res ) {
  0            
  0            
393             Future->done( $res->{outerHTML} )
394 0           });
  0            
395 0           return $html
396              
397             } else {
398             #warn "Fetching '$attribute'";
399 0           return $self->_fetch_attribute($attribute);
400             }
401             }
402              
403 0     0 1   sub get_attribute( $self, $attribute, %options ) {
  0            
  0            
  0            
  0            
404 0           $self->get_attribute_future( $attribute, %options )->get()
405             }
406              
407             =head2 C<< ->set_attribute >>
408              
409             $node->set_attribute('href' => 'https://example.com');
410              
411             Sets or creates an attribute of a node. To remove an attribute,
412             pass in the attribute value as C<undef>.
413              
414             Note that this invalidates the C<nodeId> of every node so you may or may not
415             need to refetch all other nodes or receive stale values.
416              
417             =cut
418              
419 0     0 0   sub set_attribute_future( $self, $attribute, $value ) {
  0            
  0            
  0            
  0            
420 0           my $s = $self;
421 0           weaken $s;
422 0           my $r;
423 0 0         if( defined $value ) {
424 0           $r = $self->_fetchNodeId()
425 0     0     ->then(sub( $nodeId ) {
  0            
426 0           $s->driver->send_message(
427             'DOM.setAttributeValue',
428             name => $attribute,
429             value => ''.$value,
430             nodeId => 0+$nodeId
431             )
432             })
433              
434 0           } else {
435 0           $r = $self->_fetchNodeId()
436 0     0     ->then(sub( $nodeId ) {
  0            
437 0           $s->driver->send_message('DOM.removeAttribute',
438             name => $attribute,
439             nodeId => 0+$nodeId
440             )
441             })
442 0           }
443 0           return $r
444             }
445              
446 0     0 1   sub set_attribute( $self, $attribute, $value ) {
  0            
  0            
  0            
  0            
447 0           $self->set_attribute_future( $attribute, $value )->get
448             }
449              
450             =head2 C<< ->get_tag_name >>
451              
452             print $node->get_tag_name();
453              
454             Fetches the tag name of this node
455              
456             =cut
457              
458 0     0 1   sub get_tag_name( $self ) {
  0            
  0            
459 0           my $tag = $self->nodeName;
460 0           $tag =~ s!\..*!!; # strip away the eventual classname
461 0           $tag
462             }
463              
464             =head2 C<< ->get_text >>
465              
466             print $node->get_text();
467              
468             Returns the text of the node and the contained child nodes.
469              
470             =cut
471              
472 0     0 1   sub get_text( $self ) {
  0            
  0            
473             # We need to describe all the children and concatenate their
474             # contents to retrieve the text...
475              
476             #$self->driver->send_message('DOM.describeNode',
477             # nodeId => 0+$self->nodeId, depth => -1)->then(sub($info) {
478             #
479             # my $text = '';
480             #
481             # my $collect_text = sub( $n ) {
482             # if( $n->{nodeType} == 3 ) {
483             # $text .= $n->{nodeValue} // '';
484             # };
485             # for( $n->{children}->@* ) {
486             # __SUB__->($_);
487             # }
488             # };
489             # $collect_text->( $info->{node} );
490             #
491             # Future->done( $text )
492             #})->get;
493 0           $self->get_attribute('innerText')
494             }
495              
496             =head2 C<< ->set_text >>
497              
498             $node->set_text("Hello World");
499              
500             Sets the text of the node and the contained child nodes.
501              
502             =cut
503              
504 0     0 0   sub set_text_future( $self, $value ) {
  0            
  0            
  0            
505 0           my $s = $self;
506 0           weaken $s;
507 0           my $nid = $self->_nodeId();
508 0     0     $nid->then(sub( $nodeId ) {
  0            
  0            
509 0           $s->driver->send_message('DOM.setNodeValue', nodeId => 0+$nodeId, value => $value )
510 0           });
511             }
512              
513 0     0 1   sub set_text( $self, $value ) {
  0            
  0            
  0            
514 0           $self->set_text_future->get()
515             }
516              
517             1;
518              
519             =head1 REPOSITORY
520              
521             The public repository of this module is
522             L<https://github.com/Corion/www-mechanize-chrome>.
523              
524             =head1 SUPPORT
525              
526             The public support forum of this module is L<https://perlmonks.org/>.
527              
528             =head1 TALKS
529              
530             I've given a German talk at GPW 2017, see L<http://act.yapc.eu/gpw2017/talk/7027>
531             and L<https://corion.net/talks> for the slides.
532              
533             At The Perl Conference 2017 in Amsterdam, I also presented a talk, see
534             L<http://act.perlconference.org/tpc-2017-amsterdam/talk/7022>.
535             The slides for the English presentation at TPCiA 2017 are at
536             L<https://corion.net/talks/WWW-Mechanize-Chrome/www-mechanize-chrome.en.html>.
537              
538             =head1 BUG TRACKER
539              
540             Please report bugs in this module via the Github bug queue at
541             L<https://github.com/Corion/WWW-Mechanize-Chrome/issues>
542              
543             =head1 AUTHOR
544              
545             Max Maischein C<corion@cpan.org>
546              
547             =head1 COPYRIGHT (c)
548              
549             Copyright 2010-2023 by Max Maischein C<corion@cpan.org>.
550              
551             =head1 LICENSE
552              
553             This module is released under the same terms as Perl itself.
554              
555             =cut