This is an automated email from the git hooks/post-receive script. jame-guest pushed a commit to tag v0.02 in repository libweasel-perl.
commit 30e63ef23229490d23a0a1a44d764a21d3be8d90 Author: Erik Huelsmann <[email protected]> Date: Wed Jun 15 10:20:22 2016 +0200 * Storing work-in-progress --- .proverc | 3 + CHANGES | 0 LICENSE | 339 ---------------------------------- dist.ini | 23 +++ lib/Weasel.pm | 127 +++++++++++++ lib/Weasel/DriverRole.pm | 178 ++++++++++++++++++ lib/Weasel/Element.pm | 98 ++++++++++ lib/Weasel/Element/Document.pm | 41 ++++ lib/Weasel/FindExpanders.pm | 98 ++++++++++ lib/Weasel/FindExpanders/HTML.pm | 249 +++++++++++++++++++++++++ lib/Weasel/Session.pm | 195 +++++++++++++++++++ lib/Weasel/WidgetHandlers.pm | 158 ++++++++++++++++ lib/Weasel/Widgets/HTML.pm | 35 ++++ lib/Weasel/Widgets/HTML/Button.pm | 47 +++++ lib/Weasel/Widgets/HTML/Input.pm | 65 +++++++ lib/Weasel/Widgets/HTML/Selectable.pm | 67 +++++++ t/00-load.t | 13 ++ t/01-critic.t | 46 +++++ t/02-pod-coverage.t | 29 +++ t/perlcriticrc | 2 + 20 files changed, 1474 insertions(+), 339 deletions(-) diff --git a/.proverc b/.proverc new file mode 100644 index 0000000..c6e0b45 --- /dev/null +++ b/.proverc @@ -0,0 +1,3 @@ +# default arguments for the 'prove' command +-l + diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 23cb790..0000000 --- a/LICENSE +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {description} - Copyright (C) {year} {fullname} - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - {signature of Ty Coon}, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/dist.ini b/dist.ini new file mode 100644 index 0000000..a985c0e --- /dev/null +++ b/dist.ini @@ -0,0 +1,23 @@ +name = Weasel +abstract = PHP's Mink inspired multi-protocol web-testing library for Perl +version = 0.01 +author = Erik Huelsmann <[email protected]> +copyright_holder = Erik Huelsmann +main_module = lib/Pherkin/Extension/Weasel.pm +license = Perl_5 + +[MetaResources] +bugtracker.web = https://github.com/ehuelsmann/weasel/issues +repository.url = https://github.com/ehuelsmann/weasel.git +repository.web = https://github.com/ehuelsmann/weasel +repository.type = git + +[@Basic] + + +[Prereqs] +File::Find = 0 +File::Util = 0 +Moose = 0 +Module::Runtime = 0 +List::Util = 0 diff --git a/lib/Weasel.pm b/lib/Weasel.pm new file mode 100644 index 0000000..16baa2e --- /dev/null +++ b/lib/Weasel.pm @@ -0,0 +1,127 @@ + +=head1 NAME + +Weasel - Perl's php/Mink-inspired abstracted web-driver framework + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + + use Weasel; + use Weasel::Session; + use Weasel::Driver::Selenium2; + + my $weasel = Weasel->new( + default_session => 'default', + sessions => { + default => Weasel::Session->new( + driver => Weasel::Driver::Selenium2->new(%opts), + ), + }); + + $weasel->session->get('http://localhost/index'); + +=head1 DESCRIPTION + +This module abstracts away the differences between the various +web-driver protocols, like the Mink project does for PHP. + +=cut + + +package Weasel; + +use strict; +use warnings; + +use Moose; + + +=head1 ATTRIBUTES + + +=over + +=item default_session + +The name of the default session to return from C<session>, in case +no name argument is provided. + +=cut + +has 'default_session' => (is => 'rw', + isa => 'Str', + default => 'default'); + +=item sessions + +Holds the sessions registered with the C<Weasel> instance. + +=cut + +has 'sessions' => (is => 'ro', + isa => 'HashRef[Weasel::Session]', + default => sub { {} } ); + +=back + +=head1 METHODS + +=over + +=item session([$name [, $value]]) + +Returns the session identified by C<$name>. + +If C<$value> is specified, it's associated with the given C<$name>. + +=cut + +sub session { + my ($self, $name, $value) = @_; + + $name //= $self->default_session; + $self->sessions->{$name} = $value + if defined $value; + + return $self->sessions->{$name}; +} + + +=back + +=head1 CONTRIBUTORS + +Erik Huelsmann + +=head1 MAINTAINERS + +Erik Huelsmann + +=head1 BUGS + +Bugs can be filed in the GitHub issue tracker for the Weasel project: + https://github.com/perl-weasel/weasel/issues + +=head1 SOURCE + +The source code repository for Weasel is at + https://github.com/perl-weasel/weasel + +=head1 SUPPORT + +Community support is available through +L<[email protected]|mailto:[email protected]>. + +=head1 COPYRIGHT + + (C) 2016 Erik Huelsmann + +Licensed under the same terms as Perl. + +=cut + + +1; diff --git a/lib/Weasel/DriverRole.pm b/lib/Weasel/DriverRole.pm new file mode 100644 index 0000000..c1c2a56 --- /dev/null +++ b/lib/Weasel/DriverRole.pm @@ -0,0 +1,178 @@ + +=head1 NAME + +Weasel::DriverRole - API definition for driver wrappers + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + + use Moose; + use Weasel::DriverRole; + + with 'Weasel::DriverRole'; + + ... # (re)implement the functions in Weasel::DriverRole + +=head1 DESCRIPTION + + +=cut + +package Weasel::DriverRole; + +use strict; +use warnings; + +use Carp; +use Moose::Role; + +=head1 ATTRIBUTES + +=over + +=item started + +=cut + +has 'started' => (is => 'rw', + isa => 'Bool', + default => 0); + +=back + +=head1 METHODS + +=over + +=item implements + +=cut + +sub implements { + # returning a too-old number with intent: we want warnings if this + # method hasn't been implemented by the driver + return '0.00'; +} + +=item start + +=cut + +sub start { my $self = shift; $self->started(1); } + +=item stop + +=cut + +sub stop { my $self = shift; $self->started(0); } + +=item restart + +=cut + +sub restart { my $self = shift; $self->stop; $self->start; } + +=item find_all( $parent_id, $locator, $scheme ) + +Returns the _id values for the elements to be instanciated, matching +the C<$locator> using C<scheme>. + +Depending on context, the return value is a list or an arrayref. + +=cut + +sub find_all { + croak "Abstract inteface method 'find_all' called"; +} + +=item get( $url ) + +=cut + +sub get { + croak "Abstract interface method 'get' called"; +} + +=item wait_for( $callback ) + +=cut + +sub wait_for { + croak "Abstract interface method 'wait_for' called"; +} + + +=item click( [ $element_id ] ) + +Clicks on an element if an element id is provided, or on the current +mouse location otherwise. + +=cut + +sub click { + croak "Abstract interface method 'click' called"; +} + +=item dblclick() + +Double clicks on the current mouse location. + +=cut + +sub dblclick { + croak "Abstract interface method 'dblclick' called"; +} + +=item get_attribute($element_id, $attribute_name) + +=cut + +sub get_attribute { + croak "Abstract interface method 'get_attribute' called"; +} + +=item set_attribute($element_id, $attribute_name, $value) + +=cut + +sub set_attribute { + croak "Abstract interface method 'set_attribute' called"; +} + +=item get_selected($element_id) + +=cut + +sub get_selected { + croak "Abstract interface method 'get_selected' called"; +} + +=item set_selected($element_id, $value) + +=cut + +sub set_selected { + croak "Abstract interface method 'set_selected' called"; +} + + +=back + +=head1 SEE ALSO + +L<Weasel> + +=head1 COPYRIGHT + + (C) 2016 Erik Huelsmann + +Licensed under the same terms as Perl. + +=cut + + + +1; diff --git a/lib/Weasel/Element.pm b/lib/Weasel/Element.pm new file mode 100644 index 0000000..3416236 --- /dev/null +++ b/lib/Weasel/Element.pm @@ -0,0 +1,98 @@ + +=head1 NAME + +Weasel::Element - The base HTML/Widget element class + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + + my $element = $session->page->find("./input[\@name='phone']"); + my $value = $element->send_keys('555-885-321' + +=head1 DESCRIPTION + +=cut + +package Weasel::Element; + +use strict; +use warnings; + +use Moose; + +=head1 ATTRIBUTES + +=over + +=item session + +=cut + +has session => (is => 'ro', + isa => 'Weasel::Session', + required => 1); + +=item _id + +=cut + +has _id => (is => 'ro', + required => 1); + +=back + +=head1 METHODS + +=over + +=item find($locator [, $scheme]) + + +=cut + +sub find { + my $self = shift @_; + my @rv = $self->find_all(@_); + + return shift @rv; +} + +=item find_all($locator [, $scheme]) + +=cut + +sub find_all { + my ($self, $locator, $scheme) = @_; + + # expand $locator based on framework plugins (e.g. Dojo) + return $self->session->find_all($self, $locator, $scheme); +} + +=item click() + +=cut + +sub click { + my ($self) = @_; + $self->session->click($self); +} + +=item send_keys(@keys) + +=cut + +sub send_keys { + my ($self, @keys) = @_; + + $self->session->send_keys($self, @keys); +} + +=back + +=cut + + +1; diff --git a/lib/Weasel/Element/Document.pm b/lib/Weasel/Element/Document.pm new file mode 100644 index 0000000..2cae418 --- /dev/null +++ b/lib/Weasel/Element/Document.pm @@ -0,0 +1,41 @@ + +=head1 NAME + +Weasel::Element::Document - + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + + + +=head1 DESCRIPTION + +=cut + +package Weasel::Element::Document; + +use strict; +use warnings; + +use Moose; +extends 'Weasel::Element'; + +=head1 ATTRIBUTES + +=over + +=item _id + +=cut + +has '+_id' => (required => 0, + default => '//html'); + +=back + +=cut + +1; diff --git a/lib/Weasel/FindExpanders.pm b/lib/Weasel/FindExpanders.pm new file mode 100644 index 0000000..815e550 --- /dev/null +++ b/lib/Weasel/FindExpanders.pm @@ -0,0 +1,98 @@ + +=head1 NAME + +Weasel::FindExpanders - Mapping find patterns to xpath locators + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + + use Weasel::FindExpanders qw( register_find_expander ); + + register_find_expander( + 'button', + 'HTML', + sub { + my %args = @_; + $args{text} =~ s/'/''/g; # quote the quotes (XPath 2.0) + return ".//button[text()='$args{text}']"; + }); + + $session->find($session->page, "@button|{text=>\"whatever\"}"); + +=cut + +package Weasel::FindExpanders; + +use strict; +use warnings; + +use base 'Exporter'; + +our @EXPORT_OK = qw| register_find_expander expand_finder_pattern |; + +=head1 FUNCTIONS + +=over + +=item register_find_expander($pattern_name, $group_name, &expander_function) + +Registers C<&expander_function> as an expander for C<$pattern_name> in +C<$group_name>. + +C<Weasel::Session> selects the expanders to be applied using its C<groups> +attribute. + +=cut + + +# Stores handlers as arrays per group +my %find_expanders; + +sub register_find_expander { + my ($pattern_name, $group, $expander_function) = @_; + + push @{$find_expanders{$group}{$pattern_name}}, $expander_function; +} + +=item expand_finder_pattern($pattern, $groups) + +Returns a string of concatenated (using xpath '|' operator) expansions. + +When C<$groups> is undef, all groups will be searched for C<pattern_name>. + +If the pattern doesn't match '*<pattern_name>|{<arguments>}', the pattern +is returned as the only list/arrayref element. + +=cut + +sub expand_finder_pattern { + my ($pattern, $groups) = @_; + + $groups //= keys %find_expanders; # undef --> unrestricted + return (wantarray ? ($pattern) : [ $pattern ]) + if ! ($pattern =~ m/\*([^\|]+)\|({.*})/); + my $name = $1; + # Using eval below to transform a hash-in-string to a hash efficiently + my $args = eval "$2"; ## no critic (ProhibitStringyEval) + + my @matches; + + for my $group (@$groups) { + next if ! exists $find_expanders{$group}{$name}; + + push @matches, + reverse map { $_->(%$args) } @{$find_expanders{$group}{$name}}; + } + + return join "\n|", @matches; +} + +=back + +=cut + + +1; diff --git a/lib/Weasel/FindExpanders/HTML.pm b/lib/Weasel/FindExpanders/HTML.pm new file mode 100644 index 0000000..6f7f04e --- /dev/null +++ b/lib/Weasel/FindExpanders/HTML.pm @@ -0,0 +1,249 @@ + +=head1 NAME + +Weasel::FindExpanders::HTML - + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + + use Weasel::FindExpanders::HTML; + + my $button = $session->find($session->page, "@button|{text=>\"whatever\"}"); + +=cut + +package Weasel::FindExpanders::HTML; + +use strict; +use warnings; + +use Weasel::FindExpanders qw/ register_find_expander /; + +=head1 DESCRIPTION + +=over + +=item button_expander + +Finds button tags or input tags of types submit, reset, button and image. + +Criteria: + * 'id' + * 'name' + * 'text' -- button: matches content between open and close tag + -- input: matches 'value' attribute (shown on button), + or image button's 'alt' attribute + +=cut + +sub button_expander { + my %args = @_; + + my @input_clauses; + my @btn_clauses; + if (defined $args{text}) { + push @input_clauses, "(\@alt='$args{text}' or \@value='$args{text}')"; + push @btn_clauses, "text()='$args{text}'"; + } + + for my $clause (qw/ id name /) { + if (defined $args{$clause}) { + push @input_clauses, "\@$clause='$args{$clause}'"; + push @btn_clauses, "\@$clause='$args{$clause}'"; + } + } + + my $input_clause = + (@input_clauses) ? join ' and ', ('', @input_clauses) : ''; + my $btn_clause = + (@input_clauses) ? join ' and ', @btn_clauses : ''; + return ".//input[(\@type='submit' or \@type='reset' + or \@type='image' or \@type='button') $input_clause] + | .//button[$btn_clause]"; +} + +=item checkbox_expander + +Finds input tags of type checkbox + +Criteria: + * 'id' + * 'name' + * 'value' + +=cut + +sub checkbox_expander { + my %args = @_; + + my @clauses; + for my $clause (qw/ id name value /) { + push @clauses, "\@$clause='$args{$clause}'" + if defined $clause; + } + my $clause = @clauses ? join ' and ', ('', @clauses) : ''; + return ".//input[\@type='checkbox' $clause]"; +} + +=item labelled_expander + +Finds tags for which a label has been set (using the label tag) + +Criteria: + * 'text': text of the label + * 'tag': tags for which the label has been set + +=cut + +sub labelled_expander { + my %args = @_; + + my $tag = $args{tag_name} // '*'; + my $text = $args{text}; + return ".//${tag}[id=//label[text()='$text']/\@for]"; +} + +=item link_expander + +Finds A tags with an href attribute whose text or title matches 'text' + +Criteria: + * 'text' + +=cut + +sub link_expander { + my %args = @_; + + my $text = $args{text} // ''; + # A tags with not-"no href" (thus, with an href [any href]) + return ".//a[not(not(\@href)) and text()='$text' or \@title='$text']"; +} + +=item option_expander + +Finds OPTION tags whose content matches 'text' or value matches 'value' + +Criteria: + * 'text' + * 'value' + +=cut + +sub option_expander { + my %args = @_; + + my $text = $args{text} // ''; + my $value = $args{value} // ''; + return ".//option[text()='$text' or \@value='$value']"; +} + +=item password_expander + +Finds input tags of type password + +Criteria: + * 'id' + * 'name' + +=cut + +sub password_expander { + my %args = @_; + + my @clauses; + for my $clause (qw/ id name /) { + push @clauses, "\@$clause='$args{$clause}'" + if defined $clause; + } + my $clause = @clauses ? join ' and ', ('', @clauses) : ''; + + return ".//input[\@type='password' $clause]"; +} + +=item radio_expander + +Finds input tags of type radio + +Criteria: + * 'id' + * 'name' + +=cut + +sub radio_expander { + my %args = @_; + + my @clauses; + for my $clause (qw/ id name /) { + push @clauses, "\@$clause='$args{$clause}'" + if defined $args{$clause}; + } + my $clause = join ' and ', @clauses; + + + return ".//input[\@type='radio' $clause]"; +} + +=item select_expander + +Finds select tags + +Criteria: + * 'id' + * 'name' + +=cut + +sub select_expander { + my %args = @_; + + my @clauses; + for my $clause (qw/ id name /) { + push @clauses, "\@$clause='$args{$clause}'" + if defined $clause; + } + my $clause = join ' and ', @clauses; + return ".//select[$clause]"; +} + +=item text_expander + +Finds input tags of type text or without type (which defaults to text) + +Criteria: + * 'id' + * 'name' + +=cut + +sub text_expander { + my %args = @_; + + my @clauses; + for my $clause (qw/ id name /) { + push @clauses, "\@$clause='$args{$clause}'" + if defined $clause; + } + my $clause = (@clauses) ? join ' and ', ('', @clauses) : ''; + return ".//input[(not(\@type) or \@type='text') $clause]"; +} + + +register_find_expander($_->{name}, 'HTML', $_->{expander}) + for ({ name => 'button', expander => \&button_expander }, + { name => 'checkbox', expander => \&checkbox_expander }, + { name => 'labelled', expander => \&labelled_expander }, + { name => 'link', expander => \&link_expander }, + { name => 'option', expander => \&option_expander }, + { name => 'password', expander => \&password_expander }, + { name => 'radio', expander => \&radio_expander }, + { name => 'select', expander => \&select_expander }, + { name => 'text', expander => \&text_expander }, + ); + + +1; diff --git a/lib/Weasel/Session.pm b/lib/Weasel/Session.pm new file mode 100644 index 0000000..33226bb --- /dev/null +++ b/lib/Weasel/Session.pm @@ -0,0 +1,195 @@ + +=head1 NAME + +Weasel::Session - Connection to an encapsulated test driver + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + + use Weasel; + use Weasel::Session; + use Weasel::Driver::Selenium2; + + my $weasel = Weasel->new( + default_session => 'default', + sessions => { + default => Weasel::Session->new( + driver => Weasel::Driver::Selenium2->new(%opts), + ), + }); + + $weasel->session->get('http://localhost/index'); + + +=head1 DESCRIPTION + + + +=cut + +package Weasel::Session; + + +use strict; +use warnings; + +use Moose; +use Weasel::Element::Document; +use Weasel::FindExpanders qw/ expand_finder_pattern /; +use Weasel::WidgetHandlers qw| best_match_handler_class |; + + +=head1 ATTRIBUTES + + +=over + +=item driver + +Holds a reference to the sessions's driver. + +=cut + +has 'driver' => (is => 'ro', + required => 1, + handles => { + 'start' => 'start', + 'stop' => 'stop', + 'restart' => 'restart', + 'started' => 'started', + }); + +=item widget_groups + +Contains the list of widget groups to be + +=cut + +has 'widget_groups' => (is => 'rw'); + +=item base_url + +Holds the prefix that will be prepended to every URL passed +to this API. + +=cut + +has 'base_url' => (is => 'rw', + isa => 'Str', + default => '' ); + +=item page + +=cut + +has 'page' => (is => 'ro', + isa => 'Weasel::Element::Document', + builder => '_page_builder'); + +sub _page_builder { + my $self = shift; + + return Weasel::Element::Document->new(session => $self); +} + + +=back + +=head1 METHODS + + +=over + +=item find($element, $pattern, $args) + +=cut + +sub find { + my ($self, @args) = @_; + my $rv; + + $self->wait_for( sub { return $rv = shift @{$self->find_all(@args)}; }); + + return $rv; +} + +=item find_all($element, $pattern, $args) + +=cut + +sub find_all { + my ($self, $element, $pattern, $args) = @_; + + my @rv = + map { $self->_wrap_widget($_) } + $self->driver->find_all($element->_id, + expand_finder_pattern($pattern), + $args->{scheme}); + return wantarray ? @rv : \@rv; +} + + +=item get($url) + +Loads C<$url> into the active browser window of the driver connection, +after prefixing with C<base_url>. + +=cut + +sub get { + my ($self, $url) = @_; + + $url = $self->base_url . $url; + $self->driver->get($url); +} + + +=item wait_for($callback) + +Waits until $callback->() returns true, or C<wait_timeout> expires +(if the driver supports it) -- whichever comes first.x + +=cut + +sub wait_for { + my ($self, $callback) = @_; + + $self->driver->wait_for($callback); +} + +=item _wrap_widget($_id) + +Finds all matching widget selectors to instantiate an element off of. + +In case of multiple matches, selects the most specific match +(most matched criteria). + +=cut + +sub _wrap_widget { + my ($self, $_id) = @_; + my $best_class = best_match_handler_class( + $self->driver, $_id, $self->widget_groups) // 'Weasel::Element'; + + return $best_class->new(_id => $_id, session => $self->session); +} + +=back + +=head1 SEE ALSO + +L<Weasel> + +=head1 COPYRIGHT + + (C) 2016 Erik Huelsmann + +Licensed under the same terms as Perl. + +=cut + + +1; diff --git a/lib/Weasel/WidgetHandlers.pm b/lib/Weasel/WidgetHandlers.pm new file mode 100644 index 0000000..fbd8cca --- /dev/null +++ b/lib/Weasel/WidgetHandlers.pm @@ -0,0 +1,158 @@ + +=head1 NAME + +Weasel::WidgetHandlers - Mapping elements to widget handlers + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + + use Weasel::WidgetHandlers qw( register_widget_handler ); + + register_widget_handler( + 'Weasel::Widgets::HTML::Radio', # Perl class handler + 'HTML', # Widget group + tag_name => 'input', + attributes => { + type => 'radio', + }); + + register_widget_handler( + 'Weasel::Widgets::Dojo::FilteringSelect', + 'Dojo', + tag_name => 'span', + classes => ['dijitFilteringSelect'], + attributes => { + role => 'presentation', + ... + }); + +=cut + +package Weasel::WidgetHandlers; + +use strict; +use warnings; + +use base 'Exporter'; + +use Module::Runtime qw(use_module); +use List::Util qw(max); + +our @EXPORT_OK = qw| register_widget_handler best_match_handler_class |; + +=head1 FUNCTIONS +g +=over + +=item register_widget_handler($handler_class_name, $group_name, %conditions) + +Registers C<$handler_class_name> to be the instantiated widget returned +for an element matching C<%conditions> into C<$group_name>. + +C<Weasel::Session> can select a subset of widgets to be applicable to that +session by adding a subset of available groups to that session. + +=cut + + +# Stores handlers as arrays per group +my %widget_handlers; + +sub register_widget_handler { + my ($class, $group, %conditions) = @_; + + # make sure we can use the module by pre-loading it + use_module $class; + + push @{$widget_handlers{$group}}, { + class => $class, + conditions => \%conditions, + }; +} + +=item best_match_handler_class($driver, $_id, $groups) + +Returns the best matching handler's class name, within the groups +listed in the arrayref C<$groups>, or C<undef> in case of no match. + +When C<$groups> is undef, all registered handlers will be searched. + +When multiple handlers are considered "best match", the one last added +to the group last mentioned in C<$groups> is selected. + +=cut + +sub _cached_elem_att { + my ($cache, $driver, $_id, $att) = @_; + + return (exists $cache->{$att}) + ? $cache->{$att} + : ($cache->{$att} = $driver->get_attribute($_id)); +} + +sub best_match_handler_class { + my ($driver, $_id, $groups) = @_; + + $groups //= keys %widget_handlers; # undef --> unrestricted + + my @matches; + my $elem_att_cache = {}; + my $elem_classes; + + my $tag = $driver->tag_name($_id); + for my $group (@$groups) { + my $handlers = $widget_handlers{$group}; + + handler: + for my $handler (@$handlers) { + my $conditions = $handler->{conditions}; + + next unless $tag eq $conditions->{tag_name}; + my $match_count = 1; + + if (exists $conditions->{classes}) { + %{$elem_classes} = + map { $_ => 1 } + split /\s+/, ($driver->get_attribute($_id, 'class') + // '') + unless defined $elem_classes; + + for my $class (@{$conditions->{classes}}) { + next handler + unless exists $elem_classes->{$class}; + $match_count++; + } + } + + for my $att (keys %{$conditions->{attributes}}) { + next handler + unless $conditions->{$att} eq _cached_elem_attr( + $elem_att_cache, $driver, $_id, $att); + $match_count++; + } + + push @matches, { + count => $match_count, + handler => $handler, + }; + } + } + my $max_count = max map { $_->{count} } @matches; + @matches = grep { $_->{count} == $max_count } @matches; + + warn "multiple matching handlers for element\n" + if scalar(@matches) > 1; + + my $best_match = pop @matches; + return $best_match ? $best_match->{class} : undef; +} + +=back + +=cut + + +1; diff --git a/lib/Weasel/Widgets/HTML.pm b/lib/Weasel/Widgets/HTML.pm new file mode 100644 index 0000000..2059ce9 --- /dev/null +++ b/lib/Weasel/Widgets/HTML.pm @@ -0,0 +1,35 @@ + +=head1 NAME + +Weasel::Widgets::HTML - Helper module for bulk-registration of HTML widgets + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + + use Weasel::Widgets::HTML; + + my $button = $session->page->find('//button'); + # $button is now a Weasel::Widgets::HTML::Button instance + +=head1 DESCRIPTION + + +=cut + +package Weasel::Widgets::HTML; + +use strict; +use warnings; + + +use Weasel::Widgets::HTML::Button; # button, reset, image, submit, BUTTON +use Weasel::Widgets::HTML::Selectable; # checkbox, radio, OPTION +use Weasel::Widgets::HTML::Input; # text, password, <default> + +# No widgets for file inputs and +# more importantly TEXTAREA, FORM and SELECT + +1; diff --git a/lib/Weasel/Widgets/HTML/Button.pm b/lib/Weasel/Widgets/HTML/Button.pm new file mode 100644 index 0000000..376bbeb --- /dev/null +++ b/lib/Weasel/Widgets/HTML/Button.pm @@ -0,0 +1,47 @@ + +=head1 NAME + +Weasel::Widgets::HTML::Button - Wrapper for button-like INPUT and BUTTON tags + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + + my $button = $session->page->find('./button'); + # Submit the button's form + $button->click; + +=head1 DESCRIPTION + +=cut + +package Weasel::Widgets::HTML::Button; + + +use strict; +use warnings; + +use Moose; +use Weasel::WidgetHandlers qw/ register_widget_handler /; + +extends 'Weasel::Widgets::HTML::Input'; + +register_widget_handler( + __PACKAGE__, 'HTML', + tag_name => 'input', + attributes => { + type => $_ + }) + for (qw/ submit reset button image /); + +register_widget_handler( + __PACKAGE__, 'HTML', + tag_name => 'button' + ); + + + + +1; diff --git a/lib/Weasel/Widgets/HTML/Input.pm b/lib/Weasel/Widgets/HTML/Input.pm new file mode 100644 index 0000000..253faf9 --- /dev/null +++ b/lib/Weasel/Widgets/HTML/Input.pm @@ -0,0 +1,65 @@ + +=head1 NAME + +Weasel::Widgets::HTML::Input - Parent of the INPUT, OPTION and BUTTON wrappers + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=cut + +package Weasel::Widgets::HTML::Input; + + +use strict; +use warnings; + +use Moose; +use Weasel::Element; +use Weasel::WidgetHandlers qw/ register_widget_handler /; +extends 'Weasel::Element'; + + +register_widget_handler( + __PACKAGE__, 'HTML', + tag_name => 'input', + attributes => { + type => $_, + }) + for (qw/ text password hidden /); + +register_widget_handler( + __PACKAGE__, 'HTML', + tag_name => 'input', + attributes => { + type => undef, # default input type == 'text' + }); + + +=head1 METHODS + +=over + +=item value([$value]) + +Gets the 'value' attribute; if C<$value> is provided, it is used to set the +attribute value. + +=cut + +sub value { + my ($self, $value) = @_; + + $self->session->set_attribute($self, 'value', $value) + if defined $value; + + return $self->session->get_attribute($self, 'value'); +} + + +1; diff --git a/lib/Weasel/Widgets/HTML/Selectable.pm b/lib/Weasel/Widgets/HTML/Selectable.pm new file mode 100644 index 0000000..63ae5d9 --- /dev/null +++ b/lib/Weasel/Widgets/HTML/Selectable.pm @@ -0,0 +1,67 @@ + +=head1 NAME + +Weasel::Widgets::HTML::Selectable - Wrapper for selectable elements + +=head1 VERSION + +0.01 + +=head1 SYNOPSIS + + my $selectable = $session->page->find('./option'); + $selectable->selected(1); # select option + +=head1 DESCRIPTION + + +=cut + +package Weasel::Widgets::HTML::Selectable; + + +use strict; +use warnings; + +use Moose; +use Weasel::Widgets::HTML::Input; +use Weasel::WidgetHandlers qw/ register_widget_handler /; + +extends 'Weasel::Widgets::HTML::Input'; + +register_widget_handler( + __PACKAGE__, 'HTML', + tag_name => 'input', + attributes => { + type => $_, + }) + for (qw/ radio checkbox /); + +register_widget_handler( + __PACKAGE__, 'HTML', + tag_name => 'option', + ); + +=head1 METHODS + +=over + +=item selected([$value]) + +Returns selected status of the element. If C<$value> is provided, +sets the selected status. + +=cut + +sub selected { + my ($self, $value) = @_; + + $self->session->set_selected($self, $value) + if defined $value; + + return $self->session->get_selected($self); +} + + + +1; diff --git a/t/00-load.t b/t/00-load.t new file mode 100644 index 0000000..d1ab41f --- /dev/null +++ b/t/00-load.t @@ -0,0 +1,13 @@ +#!perl + +use strict; +use warnings; + +use Test::More; + +use_ok($_) for (qw(Weasel Weasel::Session + Weasel::Element Weasel::Element::Document + Weasel::DriverRole)); + +done_testing; + diff --git a/t/01-critic.t b/t/01-critic.t new file mode 100644 index 0000000..d63ddeb --- /dev/null +++ b/t/01-critic.t @@ -0,0 +1,46 @@ +#!perl + +use strict; +use warnings; + +use File::Find; +use Perl::Critic; +use Test::More; + +sub test_files { + my ($critic, $files) = @_; + + for my $file (@$files) { + my @findings = $critic->critique($file); + + ok(scalar(@findings) == 0, "Critique for $file"); + for my $finding (@findings) { + diag($finding->description); + } + } + + return; +} + + +my @on_disk; +sub collect { + return if $File::Find::name !~ m/\.pm$/; + + my $module = $File::Find::name; + push @on_disk, $module +} +find(\&collect, 'lib/'); + +test_files(Perl::Critic->new( + -profile => 't/perlcriticrc', + -severity => 5, + -theme => '', + -include => [ +# 'Documentation::' + ]), + \@on_disk); + + +done_testing; + diff --git a/t/02-pod-coverage.t b/t/02-pod-coverage.t new file mode 100644 index 0000000..8e8e2a7 --- /dev/null +++ b/t/02-pod-coverage.t @@ -0,0 +1,29 @@ +#!perl + +use strict; +use warnings; + +use File::Find; +use File::Util; +use Test::More; +use Test::Pod::Coverage; + +my @on_disk; +sub collect { + return if $File::Find::name !~ m/\.pm$/; + + my $module = $File::Find::name; + push @on_disk, $module +} +find(\&collect, 'lib/'); + +my $sep = File::Util::SL(); +for my $f (@on_disk) { + $f =~ s/\.pm//; + $f =~ s#^lib/##; + $f =~ s#\Q$sep\E#::#g; + + pod_coverage_ok($f); +} + +done_testing; diff --git a/t/perlcriticrc b/t/perlcriticrc new file mode 100644 index 0000000..f8f9e8c --- /dev/null +++ b/t/perlcriticrc @@ -0,0 +1,2 @@ +[Modules::ProhibitEvilModules] +modules = Carp::Always Data::Dumper -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-perl/packages/libweasel-perl.git _______________________________________________ Pkg-perl-cvs-commits mailing list [email protected] http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-perl-cvs-commits
