… | |
… | |
37 | To make most of this module, or, in fact, to make any reasonable use of |
37 | To make most of this module, or, in fact, to make any reasonable use of |
38 | this module, you would need to refer to the W3C WebDriver recommendation, |
38 | this module, you would need to refer to the W3C WebDriver recommendation, |
39 | which can be found L<here|https://www.w3.org/TR/webdriver1/>: |
39 | which can be found L<here|https://www.w3.org/TR/webdriver1/>: |
40 | |
40 | |
41 | https://www.w3.org/TR/webdriver1/ |
41 | https://www.w3.org/TR/webdriver1/ |
|
|
42 | |
|
|
43 | =head2 CONVENTIONS |
|
|
44 | |
|
|
45 | Unless otherwise stated, all delays and time differences in this module |
|
|
46 | are represented as an integer number of milliseconds. |
42 | |
47 | |
43 | =cut |
48 | =cut |
44 | |
49 | |
45 | package AnyEvent::WebDriver; |
50 | package AnyEvent::WebDriver; |
46 | |
51 | |
… | |
… | |
49 | use Carp (); |
54 | use Carp (); |
50 | use JSON::XS (); |
55 | use JSON::XS (); |
51 | use AnyEvent (); |
56 | use AnyEvent (); |
52 | use AnyEvent::HTTP (); |
57 | use AnyEvent::HTTP (); |
53 | |
58 | |
54 | our $VERSION = 0.1; |
59 | our $VERSION = 0.2; |
55 | |
60 | |
56 | our $WEB_ELEMENT_IDENTIFIER = "element-6066-11e4-a52e-4f735466cecf"; |
61 | our $WEB_ELEMENT_IDENTIFIER = "element-6066-11e4-a52e-4f735466cecf"; |
57 | |
62 | |
58 | my $json = JSON::XS->new |
63 | my $json = JSON::XS->new |
59 | ->utf8; |
64 | ->utf8; |
… | |
… | |
134 | }; |
139 | }; |
135 | |
140 | |
136 | goto &$name; |
141 | goto &$name; |
137 | } |
142 | } |
138 | |
143 | |
139 | =head2 CREATING WEBDRIVER OBJECTS |
144 | =head2 WEBDRIVER OBJECTS |
140 | |
145 | |
141 | =over |
146 | =over |
142 | |
147 | |
143 | =item new AnyEvent::WebDriver key => value... |
148 | =item new AnyEvent::WebDriver key => value... |
144 | |
149 | |
… | |
… | |
196 | |
201 | |
197 | $self->delete_session |
202 | $self->delete_session |
198 | if exists $self->{sid}; |
203 | if exists $self->{sid}; |
199 | } |
204 | } |
200 | |
205 | |
|
|
206 | =item $al = $wd->actions |
|
|
207 | |
|
|
208 | Creates an action list associated with this WebDriver. See L<ACTION |
|
|
209 | LISTS>, below, for full details. |
|
|
210 | |
|
|
211 | =cut |
|
|
212 | |
|
|
213 | sub actions { |
|
|
214 | AnyEvent::WebDriver::Actions->new (wd => $_[0]) |
|
|
215 | } |
|
|
216 | |
201 | =back |
217 | =back |
202 | |
218 | |
203 | =head2 SIMPLIFIED API |
219 | =head2 SIMPLIFIED API |
204 | |
220 | |
205 | This section documents the simplified API, which is really just a very |
221 | This section documents the simplified API, which is really just a very |
… | |
… | |
407 | |
423 | |
408 | =item $handles = $wd->switch_to_frame ($frame) |
424 | =item $handles = $wd->switch_to_frame ($frame) |
409 | |
425 | |
410 | Switch to the given frame identified by C<$frame>, which must be either |
426 | Switch to the given frame identified by C<$frame>, which must be either |
411 | C<undef> to go back to the top-level browsing context, an integer to |
427 | C<undef> to go back to the top-level browsing context, an integer to |
412 | select the nth subframe, or an element object (as e.g. returned by the |
428 | select the nth subframe, or an element object. |
413 | C<element_object> method. |
|
|
414 | |
429 | |
415 | =cut |
430 | =cut |
416 | |
431 | |
417 | sub switch_to_frame_ { |
432 | sub switch_to_frame_ { |
418 | $_[0]->post_ (frame => { id => "$_[1]" }, $_[2]); |
433 | $_[0]->post_ (frame => { id => "$_[1]" }, $_[2]); |
… | |
… | |
479 | |
494 | |
480 | =over |
495 | =over |
481 | |
496 | |
482 | =cut |
497 | =cut |
483 | |
498 | |
484 | =item $element_id = $wd->find_element ($location_strategy, $selector) |
499 | =item $element = $wd->find_element ($location_strategy, $selector) |
485 | |
500 | |
486 | Finds the first element specified by the given selector and returns its |
501 | Finds the first element specified by the given selector and returns its |
487 | web element ID (the strong, not the object from the protocol). Raises an |
502 | element object. Raises an error when no element was found. |
488 | error when no element was found. |
|
|
489 | |
503 | |
490 | $element = $wd->find_element ("css selector" => "body a"); |
504 | $element = $wd->find_element ("css selector" => "body a"); |
491 | $element = $wd->find_element ("link text" => "Click Here For Porn"); |
505 | $element = $wd->find_element ("link text" => "Click Here For Porn"); |
492 | $element = $wd->find_element ("partial link text" => "orn"); |
506 | $element = $wd->find_element ("partial link text" => "orn"); |
493 | $element = $wd->find_element ("tag name" => "input"); |
507 | $element = $wd->find_element ("tag name" => "input"); |
494 | $element = $wd->find_element ("xpath" => '//input[@type="text"]'); |
508 | $element = $wd->find_element ("xpath" => '//input[@type="text"]'); |
495 | => e.g. "decddca8-5986-4e1d-8c93-efe952505a5f" |
509 | => e.g. { "element-6066-11e4-a52e-4f735466cecf" => "decddca8-5986-4e1d-8c93-efe952505a5f" } |
496 | |
510 | |
497 | =item $element_ids = $wd->find_elements ($location_strategy, $selector) |
511 | =item $elements = $wd->find_elements ($location_strategy, $selector) |
498 | |
512 | |
499 | As above, but returns an arrayref of all found element IDs. |
513 | As above, but returns an arrayref of all found element objects. |
500 | |
514 | |
501 | =item $element_id = $wd->find_element_from_element ($element_id, $location_strategy, $selector) |
515 | =item $element = $wd->find_element_from_element ($element, $location_strategy, $selector) |
502 | |
516 | |
503 | Like C<find_element>, but looks only inside the specified C<$element>. |
517 | Like C<find_element>, but looks only inside the specified C<$element>. |
504 | |
518 | |
505 | =item $element_ids = $wd->find_elements_from_element ($element_id, $location_strategy, $selector) |
519 | =item $elements = $wd->find_elements_from_element ($element, $location_strategy, $selector) |
506 | |
520 | |
507 | Like C<find_elements>, but looks only inside the specified C<$element>. |
521 | Like C<find_elements>, but looks only inside the specified C<$element>. |
508 | |
522 | |
509 | my $head = $wd->find_element ("tag name" => "head"); |
523 | my $head = $wd->find_element ("tag name" => "head"); |
510 | my $links = $wd->find_elements_from_element ($head, "tag name", "link"); |
524 | my $links = $wd->find_elements_from_element ($head, "tag name", "link"); |
511 | |
525 | |
512 | =item $element_id = $wd->get_active_element |
526 | =item $element = $wd->get_active_element |
513 | |
527 | |
514 | Returns the active element. |
528 | Returns the active element. |
515 | |
529 | |
516 | =cut |
530 | =cut |
517 | |
531 | |
518 | sub find_element_ { |
532 | sub find_element_ { |
519 | my $cb = pop; |
|
|
520 | $_[0]->post_ (element => { using => "$_[1]", value => "$_[2]" }, sub { |
533 | $_[0]->post_ (element => { using => "$_[1]", value => "$_[2]" }, $_[3]); |
521 | $cb->($_[0], $_[0] ne "200" ? $_[1] : $_[1]{$WEB_ELEMENT_IDENTIFIER}) |
|
|
522 | }); |
|
|
523 | } |
534 | } |
524 | |
535 | |
525 | sub find_elements_ { |
536 | sub find_elements_ { |
526 | my $cb = pop; |
|
|
527 | $_[0]->post_ (elements => { using => "$_[1]", value => "$_[2]" }, sub { |
537 | $_[0]->post_ (elements => { using => "$_[1]", value => "$_[2]" }, $_[3]); |
528 | $cb->($_[0], $_[0] ne "200" ? $_[1] : [ map $_->{$WEB_ELEMENT_IDENTIFIER}, @{$_[1]} ]); |
|
|
529 | }); |
|
|
530 | } |
538 | } |
531 | |
539 | |
532 | sub find_element_from_element_ { |
540 | sub find_element_from_element_ { |
533 | my $cb = pop; |
|
|
534 | $_[0]->post_ ("element/$_[1]/element" => { using => "$_[2]", value => "$_[3]" }, sub { |
541 | $_[0]->post_ ("element/$_[1]/element" => { using => "$_[2]", value => "$_[3]" }, $_[4]); |
535 | $cb->($_[0], $_[0] ne "200" ? $_[1] : $_[1]{$WEB_ELEMENT_IDENTIFIER}) |
|
|
536 | }); |
|
|
537 | } |
542 | } |
538 | |
543 | |
539 | sub find_elements_from_element_ { |
544 | sub find_elements_from_element_ { |
540 | my $cb = pop; |
|
|
541 | $_[0]->post_ ("element/$_[1]/elements" => { using => "$_[2]", value => "$_[3]" }, sub { |
545 | $_[0]->post_ ("element/$_[1]/elements" => { using => "$_[2]", value => "$_[3]" }, $_[4]); |
542 | $cb->($_[0], $_[0] ne "200" ? $_[1] : [ map $_->{$WEB_ELEMENT_IDENTIFIER}, @{$_[1]} ]); |
|
|
543 | }); |
|
|
544 | } |
546 | } |
545 | |
547 | |
546 | sub get_active_element_ { |
548 | sub get_active_element_ { |
547 | my $cb = pop; |
|
|
548 | $_[0]->get_ ("element/active" => sub { |
549 | $_[0]->get_ ("element/active" => $_[1]); |
549 | $cb->($_[0], $_[0] ne "200" ? $_[1] : $_[1]{$WEB_ELEMENT_IDENTIFIER}) |
|
|
550 | }); |
|
|
551 | } |
550 | } |
552 | |
551 | |
553 | =back |
552 | =back |
554 | |
553 | |
555 | =head3 ELEMENT STATE |
554 | =head3 ELEMENT STATE |
… | |
… | |
560 | |
559 | |
561 | =item $bool = $wd->is_element_selected |
560 | =item $bool = $wd->is_element_selected |
562 | |
561 | |
563 | Returns whether the given input or option element is selected or not. |
562 | Returns whether the given input or option element is selected or not. |
564 | |
563 | |
565 | =item $string = $wd->get_element_attribute ($element_id, $name) |
564 | =item $string = $wd->get_element_attribute ($element, $name) |
566 | |
565 | |
567 | Returns the value of the given attribute. |
566 | Returns the value of the given attribute. |
568 | |
567 | |
569 | =item $string = $wd->get_element_property ($element_id, $name) |
568 | =item $string = $wd->get_element_property ($element, $name) |
570 | |
569 | |
571 | Returns the value of the given property. |
570 | Returns the value of the given property. |
572 | |
571 | |
573 | =item $string = $wd->get_element_css_value ($element_id, $name) |
572 | =item $string = $wd->get_element_css_value ($element, $name) |
574 | |
573 | |
575 | Returns the value of the given CSS value. |
574 | Returns the value of the given CSS value. |
576 | |
575 | |
577 | =item $string = $wd->get_element_text ($element_id) |
576 | =item $string = $wd->get_element_text ($element) |
578 | |
577 | |
579 | Returns the (rendered) text content of the given element. |
578 | Returns the (rendered) text content of the given element. |
580 | |
579 | |
581 | =item $string = $wd->get_element_tag_name ($element_id) |
580 | =item $string = $wd->get_element_tag_name ($element) |
582 | |
581 | |
583 | Returns the tag of the given element. |
582 | Returns the tag of the given element. |
584 | |
583 | |
585 | =item $rect = $wd->get_element_rect ($element_id) |
584 | =item $rect = $wd->get_element_rect ($element) |
586 | |
585 | |
587 | Returns the element rect(angle) of the given element. |
586 | Returns the element rect(angle) of the given element. |
588 | |
587 | |
589 | =item $bool = $wd->is_element_enabled |
588 | =item $bool = $wd->is_element_enabled |
590 | |
589 | |
591 | Returns whether the element is enabled or not. |
590 | Returns whether the element is enabled or not. |
592 | |
591 | |
593 | =cut |
592 | =cut |
594 | |
593 | |
595 | sub is_element_selected_ { |
594 | sub is_element_selected_ { |
596 | $_[0]->get_ ("element/$_[1]/selected" => $_[2]); |
595 | $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/selected" => $_[2]); |
597 | } |
596 | } |
598 | |
597 | |
599 | sub get_element_attribute_ { |
598 | sub get_element_attribute_ { |
600 | $_[0]->get_ ("element/$_[1]/attribute/$_[2]" => $_[3]); |
599 | $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/attribute/$_[2]" => $_[3]); |
601 | } |
600 | } |
602 | |
601 | |
603 | sub get_element_property_ { |
602 | sub get_element_property_ { |
604 | $_[0]->get_ ("element/$_[1]/property/$_[2]" => $_[3]); |
603 | $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/property/$_[2]" => $_[3]); |
605 | } |
604 | } |
606 | |
605 | |
607 | sub get_element_css_value_ { |
606 | sub get_element_css_value_ { |
608 | $_[0]->get_ ("element/$_[1]/css/$_[2]" => $_[3]); |
607 | $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/css/$_[2]" => $_[3]); |
609 | } |
608 | } |
610 | |
609 | |
611 | sub get_element_text_ { |
610 | sub get_element_text_ { |
612 | $_[0]->get_ ("element/$_[1]/text" => $_[2]); |
611 | $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/text" => $_[2]); |
613 | } |
612 | } |
614 | |
613 | |
615 | sub get_element_tag_name_ { |
614 | sub get_element_tag_name_ { |
616 | $_[0]->get_ ("element/$_[1]/name" => $_[2]); |
615 | $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/name" => $_[2]); |
617 | } |
616 | } |
618 | |
617 | |
619 | sub get_element_rect_ { |
618 | sub get_element_rect_ { |
620 | $_[0]->get_ ("element/$_[1]/rect" => $_[2]); |
619 | $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/rect" => $_[2]); |
621 | } |
620 | } |
622 | |
621 | |
623 | sub is_element_enabled_ { |
622 | sub is_element_enabled_ { |
624 | $_[0]->get_ ("element/$_[1]/enabled" => $_[2]); |
623 | $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/enabled" => $_[2]); |
625 | } |
624 | } |
626 | |
625 | |
627 | =back |
626 | =back |
628 | |
627 | |
629 | =head3 ELEMENT INTERACTION |
628 | =head3 ELEMENT INTERACTION |
630 | |
629 | |
631 | =over |
630 | =over |
632 | |
631 | |
633 | =cut |
632 | =cut |
634 | |
633 | |
635 | =item $wd->element_click ($element_id) |
634 | =item $wd->element_click ($element) |
636 | |
635 | |
637 | Clicks the given element. |
636 | Clicks the given element. |
638 | |
637 | |
639 | =item $wd->element_clear ($element_id) |
638 | =item $wd->element_clear ($element) |
640 | |
639 | |
641 | Clear the contents of the given element. |
640 | Clear the contents of the given element. |
642 | |
641 | |
643 | =item $wd->element_send_keys ($element_id, $text) |
642 | =item $wd->element_send_keys ($element, $text) |
644 | |
643 | |
645 | Sends the given text as key events to the given element. |
644 | Sends the given text as key events to the given element. |
646 | |
645 | |
647 | =cut |
646 | =cut |
648 | |
647 | |
649 | sub element_click_ { |
648 | sub element_click_ { |
650 | $_[0]->post_ ("element/$_[1]/click" => undef, $_[2]); |
649 | $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/click" => undef, $_[2]); |
651 | } |
650 | } |
652 | |
651 | |
653 | sub element_clear_ { |
652 | sub element_clear_ { |
654 | $_[0]->post_ ("element/$_[1]/clear" => undef, $_[2]); |
653 | $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/clear" => undef, $_[2]); |
655 | } |
654 | } |
656 | |
655 | |
657 | sub element_send_keys_ { |
656 | sub element_send_keys_ { |
658 | $_[0]->post_ ("element/$_[1]/value" => { text => "$_[2]" }, $_[3]); |
657 | $_[0]->post_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/value" => { text => "$_[2]" }, $_[3]); |
659 | } |
658 | } |
660 | |
659 | |
661 | =back |
660 | =back |
662 | |
661 | |
663 | =head3 DOCUMENT HANDLING |
662 | =head3 DOCUMENT HANDLING |
… | |
… | |
763 | =cut |
762 | =cut |
764 | |
763 | |
765 | =item $wd->perform_actions ($actions) |
764 | =item $wd->perform_actions ($actions) |
766 | |
765 | |
767 | Perform the given actions (an arrayref of action specifications simulating |
766 | Perform the given actions (an arrayref of action specifications simulating |
768 | user activity). For further details, read the spec. |
767 | user activity, or an C<AnyEvent::WebDriver::Actions> object). For further |
|
|
768 | details, read the spec or the section L<ACTION LISTS>, below. |
769 | |
769 | |
770 | An example to get you started: |
770 | An example to get you started (see the next example for a mostly |
|
|
771 | equivalent example using the C<AnyEvent::WebDriver::Actions> helper API): |
771 | |
772 | |
772 | $wd->navigate_to ("https://duckduckgo.com/html"); |
773 | $wd->navigate_to ("https://duckduckgo.com/html"); |
773 | $wd->set_timeouts ({ implicit => 10000 }); |
|
|
774 | my $input = $wd->find_element ("css selector", 'input[type="text"]'); |
774 | my $input = $wd->find_element ("css selector", 'input[type="text"]'); |
775 | $wd->perform_actions ([ |
775 | $wd->perform_actions ([ |
776 | { |
776 | { |
777 | id => "myfatfinger", |
777 | id => "myfatfinger", |
778 | type => "pointer", |
778 | type => "pointer", |
779 | pointerType => "touch", |
779 | pointerType => "touch", |
780 | actions => [ |
780 | actions => [ |
781 | { type => "pointerMove", duration => 100, origin => $wd->element_object ($input), x => 40, y => 5 }, |
781 | { type => "pointerMove", duration => 100, origin => $input, x => 40, y => 5 }, |
782 | { type => "pointerDown", button => 1 }, |
782 | { type => "pointerDown", button => 1 }, |
783 | { type => "pause", duration => 40 }, |
783 | { type => "pause", duration => 40 }, |
784 | { type => "pointerUp", button => 1 }, |
784 | { type => "pointerUp", button => 1 }, |
785 | ], |
785 | ], |
786 | }, |
786 | }, |
… | |
… | |
806 | { type => "pause", duration => 5000 }, |
806 | { type => "pause", duration => 5000 }, |
807 | ], |
807 | ], |
808 | }, |
808 | }, |
809 | ]); |
809 | ]); |
810 | |
810 | |
|
|
811 | And here is essentially the same (except for fewer pauses) example as |
|
|
812 | above, using the much simpler C<AnyEvent::WebDriver::Actions> API. Note |
|
|
813 | that the pointer up and key down event happen concurrently in this |
|
|
814 | example: |
|
|
815 | |
|
|
816 | $wd->navigate_to ("https://duckduckgo.com/html"); |
|
|
817 | my $input = $wd->find_element ("css selector", 'input[type="text"]'); |
|
|
818 | $wd->actions |
|
|
819 | ->move ($input, 40, 5, "touch1") |
|
|
820 | ->click; |
|
|
821 | ->key ("a"); |
|
|
822 | ->key ("b"); |
|
|
823 | ->pause (2000); |
|
|
824 | ->key ("\x{E007}") |
|
|
825 | ->pause (5000); |
|
|
826 | ->perform; |
|
|
827 | |
811 | =item $wd->release_actions |
828 | =item $wd->release_actions |
812 | |
829 | |
813 | Release all keys and pointer buttons currently depressed. |
830 | Release all keys and pointer buttons currently depressed. |
814 | |
831 | |
815 | =cut |
832 | =cut |
816 | |
833 | |
817 | sub perform_actions_ { |
834 | sub perform_actions_ { |
|
|
835 | if (UNIVERSAL::isa $_[1], AnyEvent::WebDriver::Actions::) { |
|
|
836 | my ($actions, $duration) = $_[1]->compile; |
|
|
837 | local $_[0]{timeout} = $_[0]{timeout} + $duration * 1e-3; |
|
|
838 | $_[0]->post_ (actions => { actions => $actions }, $_[2]); |
|
|
839 | } else { |
818 | $_[0]->post_ (actions => { actions => $_[1] }, $_[2]); |
840 | $_[0]->post_ (actions => { actions => $_[1] }, $_[2]); |
|
|
841 | } |
819 | } |
842 | } |
820 | |
843 | |
821 | sub release_actions_ { |
844 | sub release_actions_ { |
822 | $_[0]->delete_ (actions => $_[1]); |
845 | $_[0]->delete_ (actions => $_[1]); |
823 | } |
846 | } |
… | |
… | |
875 | |
898 | |
876 | =item $wd->take_screenshot |
899 | =item $wd->take_screenshot |
877 | |
900 | |
878 | Create a screenshot, returning it as a PNG image in a C<data:> URL. |
901 | Create a screenshot, returning it as a PNG image in a C<data:> URL. |
879 | |
902 | |
880 | =item $wd->take_element_screenshot ($element_id) |
903 | =item $wd->take_element_screenshot ($element) |
881 | |
904 | |
882 | Accept a simple dialog, if present. |
905 | Accept a simple dialog, if present. |
883 | |
906 | |
884 | =cut |
907 | =cut |
885 | |
908 | |
886 | sub take_screenshot_ { |
909 | sub take_screenshot_ { |
887 | $_[0]->get_ (screenshot => $_[1]); |
910 | $_[0]->get_ (screenshot => $_[1]); |
888 | } |
911 | } |
889 | |
912 | |
890 | sub take_element_screenshot_ { |
913 | sub take_element_screenshot_ { |
891 | $_[0]->get_ ("element/$_[1]/screenshot" => $_[2]); |
914 | $_[0]->get_ ("element/$_[1]{$WEB_ELEMENT_IDENTIFIER}/screenshot" => $_[2]); |
892 | } |
915 | } |
893 | |
916 | |
894 | =back |
917 | =back |
895 | |
918 | |
896 | =head2 HELPER METHODS |
919 | =head2 ACTION LISTS |
897 | |
920 | |
|
|
921 | Action lists can be quite complicated. Or at least it took a while for |
|
|
922 | me to twist my head around them. Basically, an action list consists of a |
|
|
923 | number of sources representing devices (such as a finger, a mouse, a pen |
|
|
924 | or a keyboard) and a list of actions for each source. |
|
|
925 | |
|
|
926 | An action can be a key press, a pointer move or a pause (time |
|
|
927 | delay). Actions from different sources can happen "at the same time", |
|
|
928 | while actions from a single source are executed in order. |
|
|
929 | |
|
|
930 | While you can provide an action list manually, it is (hopefully) less |
|
|
931 | cumbersome to use the API described in this section to create them. |
|
|
932 | |
|
|
933 | The basic process of creating and performing actions is to create a new |
|
|
934 | action list, adding action sources, followed by adding actions. Finally |
|
|
935 | you would C<perform> those actions on the WebDriver. |
|
|
936 | |
|
|
937 | Virtual time progresses as long as you add actions to the same event |
|
|
938 | source. Adding events to different sources are considered to happen |
|
|
939 | concurrently. If you want to force time to progress, you can do this using |
|
|
940 | a call to C<< ->pause (0) >>. |
|
|
941 | |
|
|
942 | Most methods here are designed to chain, i.e. they return the web actions |
|
|
943 | object, to simplify multiple calls. |
|
|
944 | |
|
|
945 | For example, to simulate a mouse click to an input element, followed by |
|
|
946 | entering some text and pressing enter, you can use this: |
|
|
947 | |
|
|
948 | $wd->actions |
|
|
949 | ->click (1, 100) |
|
|
950 | ->type ("some text") |
|
|
951 | ->key ("Enter") |
|
|
952 | ->perform; |
|
|
953 | |
|
|
954 | By default, keyboard and mouse input sources are provided. You can create |
|
|
955 | your own sources and use them when adding events. The above example could |
|
|
956 | be more verbosely written like this: |
|
|
957 | |
|
|
958 | $wd->actions |
|
|
959 | ->click (1, 100, "mouse") |
|
|
960 | ->type ("some text") |
|
|
961 | ->key ("Enter") |
|
|
962 | ->perform; |
|
|
963 | |
|
|
964 | |
|
|
965 | |
|
|
966 | #TODO verboser example |
|
|
967 | |
|
|
968 | When you specify the event source expliticly it will switch the current |
|
|
969 | "focus" for this class of device (all keyboards are in one class, all |
|
|
970 | pointer-like devices such as mice/fingers/pens are in one class), so you |
|
|
971 | don't have to specify the source for subsequent actions. |
|
|
972 | |
|
|
973 | When you use the sources C<keyboard>, C<mouse>, C<touch1>..C<touch3>, |
|
|
974 | C<pen> without defining them, then a suitable default source will be |
|
|
975 | created for them. |
|
|
976 | |
898 | =over |
977 | =over 4 |
899 | |
978 | |
900 | =cut |
979 | =cut |
901 | |
980 | |
902 | =item $object = AnyEvent::WebDriver->element_object ($element_id) |
981 | package AnyEvent::WebDriver::Actions; |
903 | |
982 | |
904 | =item $object = $wd->element_object ($element_id) |
983 | =item $al = new AnyEvent::WebDriver::Actions |
905 | |
984 | |
906 | Encoding element IDs in data structures is done by representing them as an |
985 | Create a new empty action list object. More often you would use the C<< |
907 | object with a special key and the element ID as value. This helper method |
986 | $sel->action_list >> method to create one that is already associated with |
908 | does this for you. |
987 | a given web driver. |
909 | |
988 | |
910 | =cut |
989 | =cut |
911 | |
990 | |
912 | sub element_object { |
991 | sub new { |
913 | +{ $WEB_ELEMENT_IDENTIFIER => $_[1] } |
992 | my ($class, %kv) = @_; |
|
|
993 | |
|
|
994 | $kv{last_kbd} = "keyboard"; |
|
|
995 | $kv{last_ptr} = "mouse"; |
|
|
996 | |
|
|
997 | bless \%kv, $class |
|
|
998 | } |
|
|
999 | |
|
|
1000 | =item $al = $al->source ($id, $type, key => value...) |
|
|
1001 | |
|
|
1002 | The first time you call this with a givne ID, this defines the event |
|
|
1003 | source using the extra parameters. Subsequent calls merely switch the |
|
|
1004 | current source for its event class. |
|
|
1005 | |
|
|
1006 | It's not an error to define built-in sources (such as C<keyboard> or |
|
|
1007 | C<touch1>) differently then the defaults. |
|
|
1008 | |
|
|
1009 | Example: define a new touch device called C<fatfinger>. |
|
|
1010 | |
|
|
1011 | $al->source (fatfinger => "pointer", pointerType => "touch"); |
|
|
1012 | |
|
|
1013 | Example: switchdefine a new touch device called C<fatfinger>. |
|
|
1014 | |
|
|
1015 | $al->source (fatfinger => "pointer", pointerType => "touch"); |
|
|
1016 | |
|
|
1017 | =cut |
|
|
1018 | |
|
|
1019 | sub _default_source { |
|
|
1020 | my ($source) = @_; |
|
|
1021 | |
|
|
1022 | $source eq "keyboard" ? { actions => [], id => $source, type => "key" } |
|
|
1023 | : $source eq "mouse" ? { actions => [], id => $source, type => "pointer", pointerType => "mouse" } |
|
|
1024 | : $source eq "touch" ? { actions => [], id => $source, type => "pointer", pointerType => "touch" } |
|
|
1025 | : $source eq "pen" ? { actions => [], id => $source, type => "pointer", pointerType => "pen" } |
|
|
1026 | : Carp::croak "AnyEvent::WebDriver::Actions: event source '$source' not defined" |
|
|
1027 | } |
|
|
1028 | |
|
|
1029 | my %source_class = ( |
|
|
1030 | key => "kbd", |
|
|
1031 | pointer => "ptr", |
|
|
1032 | ); |
|
|
1033 | |
|
|
1034 | sub source { |
|
|
1035 | my ($self, $id, $type, %kv) = @_; |
|
|
1036 | |
|
|
1037 | if (defined $type) { |
|
|
1038 | !exists $self->{source}{$id} |
|
|
1039 | or Carp::croak "AnyEvent::WebDriver::Actions: source '$id' already defined"; |
|
|
1040 | |
|
|
1041 | $kv{id} = $id; |
|
|
1042 | $kv{type} = $type; |
|
|
1043 | $kv{actions} = []; |
|
|
1044 | |
|
|
1045 | $self->{source}{$id} = \%kv; |
|
|
1046 | } |
|
|
1047 | |
|
|
1048 | my $source = $self->{source}{$id} ||= _default_source $id; |
|
|
1049 | |
|
|
1050 | my $last = $source_class{$source->{type}} // "xxx"; |
|
|
1051 | |
|
|
1052 | $self->{"last_$last"} = $id; |
|
|
1053 | |
|
|
1054 | $self |
|
|
1055 | } |
|
|
1056 | |
|
|
1057 | sub _add { |
|
|
1058 | my ($self, $source, $sourcetype, $type, %kv) = @_; |
|
|
1059 | |
|
|
1060 | my $last = \$self->{"last_$sourcetype"}; |
|
|
1061 | |
|
|
1062 | $source |
|
|
1063 | ? ($$last = $source) |
|
|
1064 | : ($source = $$last); |
|
|
1065 | |
|
|
1066 | my $source = $self->{source}{$source} ||= _default_source $source; |
|
|
1067 | |
|
|
1068 | my $al = $source->{actions}; |
|
|
1069 | |
|
|
1070 | push @$al, { type => "pause" } |
|
|
1071 | while @$al < $self->{tick} - 1; |
|
|
1072 | |
|
|
1073 | $kv{type} = $type; |
|
|
1074 | |
|
|
1075 | push @{ $source->{actions} }, \%kv; |
|
|
1076 | |
|
|
1077 | $self->{tick_duration} = $kv{duration} |
|
|
1078 | if $kv{duration} > $self->{tick_duration}; |
|
|
1079 | |
|
|
1080 | if ($self->{tick} != @$al) { |
|
|
1081 | $self->{tick} = @$al; |
|
|
1082 | $self->{duration} += delete $self->{tick_duration}; |
|
|
1083 | } |
|
|
1084 | |
|
|
1085 | $self |
|
|
1086 | } |
|
|
1087 | |
|
|
1088 | =item $al = $al->pause ($duration) |
|
|
1089 | |
|
|
1090 | Creates a pause with the given duration. Makes sure that time progresses |
|
|
1091 | in any case, even when C<$duration> is C<0>. |
|
|
1092 | |
|
|
1093 | =cut |
|
|
1094 | |
|
|
1095 | sub pause { |
|
|
1096 | my ($self, $duration) = @_; |
|
|
1097 | |
|
|
1098 | $self->{tick_duration} = $duration |
|
|
1099 | if $duration > $self->{tick_duration}; |
|
|
1100 | |
|
|
1101 | $self->{duration} += delete $self->{tick_duration}; |
|
|
1102 | |
|
|
1103 | # find the source with the longest list |
|
|
1104 | |
|
|
1105 | for my $source (values %{ $self->{source} }) { |
|
|
1106 | if (@{ $source->{actions} } == $self->{tick}) { |
|
|
1107 | # this source is one of the longest |
|
|
1108 | |
|
|
1109 | # create a pause event only if $duration is non-zero... |
|
|
1110 | push @{ $source->{actions} }, { type => "pause", duration => $duration*1 } |
|
|
1111 | if $duration; |
|
|
1112 | |
|
|
1113 | # ... but advance time in any case |
|
|
1114 | ++$self->{tick}; |
|
|
1115 | |
|
|
1116 | return $self; |
|
|
1117 | } |
|
|
1118 | } |
|
|
1119 | |
|
|
1120 | # no event sources are longest. so advance time in any case |
|
|
1121 | ++$self->{tick}; |
|
|
1122 | |
|
|
1123 | Carp::croak "AnyEvent::WebDriver::Actions: multiple pause calls in a row not (yet) supported" |
|
|
1124 | if $duration; |
|
|
1125 | |
|
|
1126 | $self |
|
|
1127 | } |
|
|
1128 | |
|
|
1129 | =item $al = $al->pointer_down ($button, $source) |
|
|
1130 | |
|
|
1131 | =item $al = $al->pointer_up ($button, $source) |
|
|
1132 | |
|
|
1133 | Press or release the given button. C<$button> defaults to C<1>. |
|
|
1134 | |
|
|
1135 | =item $al = $al->click ($button, $source) |
|
|
1136 | |
|
|
1137 | Convenience function that creates a button press and release action |
|
|
1138 | without any delay between them. C<$button> defaults to C<1>. |
|
|
1139 | |
|
|
1140 | =item $al = $al->doubleclick ($button, $source) |
|
|
1141 | |
|
|
1142 | Convenience function that creates two button press and release action |
|
|
1143 | pairs in a row, with no unnecessary delay between them. C<$button> |
|
|
1144 | defaults to C<1>. |
|
|
1145 | |
|
|
1146 | =cut |
|
|
1147 | |
|
|
1148 | sub pointer_down { |
|
|
1149 | my ($self, $button, $source) = @_; |
|
|
1150 | |
|
|
1151 | $self->_add ($source, ptr => pointerDown => button => ($button // 1)*1) |
|
|
1152 | } |
|
|
1153 | |
|
|
1154 | sub pointer_up { |
|
|
1155 | my ($self, $button, $source) = @_; |
|
|
1156 | |
|
|
1157 | $self->_add ($source, ptr => pointerUp => button => ($button // 1)*1) |
|
|
1158 | } |
|
|
1159 | |
|
|
1160 | sub click { |
|
|
1161 | my ($self, $button, $source) = @_; |
|
|
1162 | |
|
|
1163 | $self |
|
|
1164 | ->pointer_down ($button, $source) |
|
|
1165 | ->pointer_up ($button) |
|
|
1166 | } |
|
|
1167 | |
|
|
1168 | sub doubleclick { |
|
|
1169 | my ($self, $button, $source) = @_; |
|
|
1170 | |
|
|
1171 | $self |
|
|
1172 | ->click ($button, $source) |
|
|
1173 | ->click ($button) |
|
|
1174 | } |
|
|
1175 | |
|
|
1176 | =item $al = $al->move ($button, $origin, $x, $y, $duration, $source) |
|
|
1177 | |
|
|
1178 | Moves a pointer to the given position, relative to origin (either |
|
|
1179 | "viewport", "pointer" or an element object. |
|
|
1180 | |
|
|
1181 | =cut |
|
|
1182 | |
|
|
1183 | sub move { |
|
|
1184 | my ($self, $origin, $x, $y, $duration, $source) = @_; |
|
|
1185 | |
|
|
1186 | $self->_add ($source, ptr => pointerMove => |
|
|
1187 | origin => $origin, x => $x*1, y => $y*1, duration => $duration*1) |
|
|
1188 | } |
|
|
1189 | |
|
|
1190 | =item $al = $al->keyDown ($key, $source) |
|
|
1191 | |
|
|
1192 | =item $al = $al->keyUp ($key, $source) |
|
|
1193 | |
|
|
1194 | Press or release the given key. |
|
|
1195 | |
|
|
1196 | =cut |
|
|
1197 | |
|
|
1198 | sub key_down { |
|
|
1199 | my ($self, $key, $source) = @_; |
|
|
1200 | |
|
|
1201 | $self->_add ($source, kbd => keyDown => value => $key) |
|
|
1202 | } |
|
|
1203 | |
|
|
1204 | sub key_up { |
|
|
1205 | my ($self, $key, $source) = @_; |
|
|
1206 | |
|
|
1207 | $self->_add ($source, kbd => keyUp => value => $key) |
|
|
1208 | } |
|
|
1209 | |
|
|
1210 | sub key { |
|
|
1211 | my ($self, $key, $source) = @_; |
|
|
1212 | |
|
|
1213 | $self |
|
|
1214 | ->key_down ($key, $source) |
|
|
1215 | ->key_up ($key) |
|
|
1216 | } |
|
|
1217 | |
|
|
1218 | =item $al->perform ($wd) |
|
|
1219 | |
|
|
1220 | Finaluses and compiles the list, if not done yet, and calls C<< |
|
|
1221 | $wd->perform >> with it. |
|
|
1222 | |
|
|
1223 | If C<$wd> is undef, and the action list was created using the C<< |
|
|
1224 | $wd->actions >> method, then perform it against that WebDriver object. |
|
|
1225 | |
|
|
1226 | There is no underscore variant - call the C<perform_actions_> method with |
|
|
1227 | the action object instead. |
|
|
1228 | |
|
|
1229 | =item $al->perform_release ($wd) |
|
|
1230 | |
|
|
1231 | Exactly like C<perform>, but additionally call C<release_actions> |
|
|
1232 | afterwards. |
|
|
1233 | |
|
|
1234 | =cut |
|
|
1235 | |
|
|
1236 | sub perform { |
|
|
1237 | my ($self, $wd) = @_; |
|
|
1238 | |
|
|
1239 | ($wd //= $self->{wd})->perform_actions ($self) |
|
|
1240 | } |
|
|
1241 | |
|
|
1242 | sub perform_release { |
|
|
1243 | my ($self, $wd) = @_; |
|
|
1244 | |
|
|
1245 | ($wd //= $self->{wd})->perform_actions ($self); |
|
|
1246 | $wd->release_actions; |
|
|
1247 | } |
|
|
1248 | |
|
|
1249 | =item ($actions, $duration) = $al->compile |
|
|
1250 | |
|
|
1251 | Finalises and compiles the list, if not done yet, and returns an actions |
|
|
1252 | object suitable for calls to C<< $wd->perform_actions >>. When called in |
|
|
1253 | list context, additionally returns the total duration of the action list. |
|
|
1254 | |
|
|
1255 | Since building large action lists can take nontrivial amounts of time, |
|
|
1256 | it can make sense to build an action list only once and then perform it |
|
|
1257 | multiple times. |
|
|
1258 | |
|
|
1259 | Actions must not be added after compiling a list. |
|
|
1260 | |
|
|
1261 | =cut |
|
|
1262 | |
|
|
1263 | sub compile { |
|
|
1264 | my ($self) = @_; |
|
|
1265 | |
|
|
1266 | $self->{duration} += delete $self->{tick_duration}; |
|
|
1267 | |
|
|
1268 | delete $self->{tick}; |
|
|
1269 | delete $self->{last_kbd}; |
|
|
1270 | delete $self->{last_ptr}; |
|
|
1271 | |
|
|
1272 | $self->{actions} ||= [values %{ delete $self->{source} }]; |
|
|
1273 | |
|
|
1274 | wantarray |
|
|
1275 | ? ($self->{actions}, $self->{duration}) |
|
|
1276 | : $self->{actions} |
914 | } |
1277 | } |
915 | |
1278 | |
916 | =back |
1279 | =back |
917 | |
1280 | |
918 | =head2 EVENT BASED API |
1281 | =head2 EVENT BASED API |
… | |
… | |
1017 | |
1380 | |
1018 | Example: implement C<execute_script>, which needs some parameters: |
1381 | Example: implement C<execute_script>, which needs some parameters: |
1019 | |
1382 | |
1020 | $results = $wd->post ("execute/sync" => { script => "$javascript", args => [] }); |
1383 | $results = $wd->post ("execute/sync" => { script => "$javascript", args => [] }); |
1021 | |
1384 | |
1022 | Example: call C<find_elements> to find all C<IMG> elements, stripping the |
1385 | Example: call C<find_elements> to find all C<IMG> elements: |
1023 | returned element objects to only return the element ID strings: |
|
|
1024 | |
1386 | |
1025 | my $elems = $wd->post (elements => { using => "css selector", value => "img" }); |
1387 | $elems = $wd->post (elements => { using => "css selector", value => "img" }); |
1026 | |
|
|
1027 | # yes, the W3C found an interesting way around the typelessness of JSON |
|
|
1028 | $_ = $_->{"element-6066-11e4-a52e-4f735466cecf"} |
|
|
1029 | for @$elems; |
|
|
1030 | |
1388 | |
1031 | =cut |
1389 | =cut |
1032 | |
1390 | |
1033 | =head1 HISTORY |
1391 | =head1 HISTORY |
1034 | |
1392 | |