Weasel-0.11000755001750001750 013076472546 14352 5ustar00ehuelsmannehuelsmann000000000000README100644001750001750 61313076472546 15273 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11 This archive contains the distribution Weasel, version 0.11: PHP's Mink inspired multi-protocol web-testing library for Perl This software is copyright (c) 2017 by Erik Huelsmann. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. This README file was generated by Dist::Zilla::Plugin::Readme v6.008. CHANGES100644001750001750 250213076472546 15425 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11 ** 0.11 / 2017-04-21 - Fix expansion of *text HTML expander when only one of 'id' or 'name' given ** 0.10 / 2016-09-09 - Repair <5.14 compatibility - Add new 'get_page_source' api to the driver - Add driver API version check during session creation ** 0.09 / 2016-09-04 - Set the correct version number in Weasel.pm ** 0.08 / 2016-08-26 - Add the possibility to use environment variables for base_url ** 0.07 / 2016-08-20 - Correctly update the version numbers in lib/Weasel.pm ** 0.06 / 2016-08-20 - Add key codes in order to send special keys to elements - Add helper method on elements: has_class - Move development time tests to 'xt/' directory (fixes #1) ** 0.05 / 2016-07-08 - Change behaviour of Selectable widget (Radiobutton, Checkbox and Option) to return the empty string (false) when not selected for the 'value' attribute ** 0.04 / 2016-07-04 - Fix timing problem initializing Session's 'page' attribute (depends on 'page_class') by making it lazy ** 0.03 / 2016-07-03 - Allow overriding the class of the object instantiated into the 'page' attribute ** 0.02 / 2016-06-22 (TRIAL) - Adjusted dependencies based on testing in clean VM ** 0.01 / 2016-06-22 (TRIAL) - Initial release to replace LedgerSMB's (https://github.com/ledgersmb/LedgerSMB) test code LICENSE100644001750001750 4366313076472546 15474 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11This software is copyright (c) 2017 by Erik Huelsmann. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. Terms of the Perl programming language system itself a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or b) the "Artistic License" --- The GNU General Public License, Version 1, February 1989 --- This software is Copyright (c) 2017 by Erik Huelsmann. This is free software, licensed under: The GNU General Public License, Version 1, February 1989 GNU GENERAL PUBLIC LICENSE Version 1, February 1989 Copyright (C) 1989 Free Software Foundation, Inc. 51 Franklin St, 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 license agreements of most software companies try to keep users at the mercy of those companies. By contrast, our 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. The General Public License applies to the Free Software Foundation's software and to any other program whose authors commit to using it. You can use it for your programs, too. When we speak of free software, we are referring to freedom, not price. Specifically, the General Public License is designed to make sure that you have the freedom to give away or sell copies of free software, 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 a 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 tell them 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. 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 Agreement 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 work containing the Program or a portion of it, either verbatim or with modifications. Each licensee is addressed as "you". 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 General Public License and to the absence of any warranty; and give any other recipients of the Program a copy of this General Public License along with the Program. You may charge a fee for the physical act of transferring a copy. 2. You may modify your copy or copies of the Program or any portion of it, and copy and distribute such modifications under the terms of Paragraph 1 above, provided that you also do the following: a) cause the modified files to carry prominent notices stating that you changed the files and the date of any change; and b) cause the whole of any work that you distribute or publish, that in whole or in part contains the Program or any part thereof, either with or without modifications, to be licensed at no charge to all third parties under the terms of this General Public License (except that you may choose to grant warranty protection to some or all third parties, at your option). c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the simplest and most usual 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 General Public License. d) 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. Mere aggregation of another independent work with the Program (or its derivative) on a volume of a storage or distribution medium does not bring the other work under the scope of these terms. 3. You may copy and distribute the Program (or a portion or derivative of it, under Paragraph 2) in object code or executable form under the terms of Paragraphs 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 Paragraphs 1 and 2 above; or, b) accompany it with a written offer, valid for at least three years, to give any third party free (except for a nominal charge for the cost of distribution) a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Paragraphs 1 and 2 above; or, c) accompany it with the information you received as to where the corresponding source code may be obtained. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form alone.) Source code for a work means the preferred form of the work for making modifications to it. For an executable file, complete source code means all the source code for all modules it contains; but, as a special exception, it need not include source code for modules which are standard libraries that accompany the operating system on which the executable file runs, or for standard header files or definitions files that accompany that operating system. 4. You may not copy, modify, sublicense, distribute or transfer the Program except as expressly provided under this General Public License. Any attempt otherwise to copy, modify, sublicense, distribute or transfer the Program is void, and will automatically terminate your rights to use the Program under this License. However, parties who have received copies, or rights to use copies, from you under this General Public License will not have their licenses terminated so long as such parties remain in full compliance. 5. By copying, distributing or modifying 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. 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. 7. 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 the 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 the license, you may choose any version ever published by the Free Software Foundation. 8. 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 9. 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. 10. 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 Appendix: 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 humanity, 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. Copyright (C) 19yy 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 1, 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) 19xx 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 a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (a program to direct compilers to make passes at assemblers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice That's all there is to it! --- The Artistic License 1.0 --- This software is Copyright (c) 2017 by Erik Huelsmann. This is free software, licensed under: The Artistic License 1.0 The Artistic License Preamble The intent of this document is to state the conditions under which a Package may be copied, such that the Copyright Holder maintains some semblance of artistic control over the development of the package, while giving the users of the package the right to use and distribute the Package in a more-or-less customary fashion, plus the right to make reasonable modifications. Definitions: - "Package" refers to the collection of files distributed by the Copyright Holder, and derivatives of that collection of files created through textual modification. - "Standard Version" refers to such a Package if it has not been modified, or has been modified in accordance with the wishes of the Copyright Holder. - "Copyright Holder" is whoever is named in the copyright or copyrights for the package. - "You" is you, if you're thinking about copying or distributing this Package. - "Reasonable copying fee" is whatever you can justify on the basis of media cost, duplication charges, time of people involved, and so on. (You will not be required to justify it to the Copyright Holder, but only to the computing community at large as a market that must bear the fee.) - "Freely Available" means that no fee is charged for the item itself, though there may be fees involved in handling the item. It also means that recipients of the item may redistribute it under the same conditions they received it. 1. You may make and give away verbatim copies of the source form of the Standard Version of this Package without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may apply bug fixes, portability fixes and other modifications derived from the Public Domain or from the Copyright Holder. A Package modified in such a way shall still be considered the Standard Version. 3. You may otherwise modify your copy of this Package in any way, provided that you insert a prominent notice in each changed file stating how and when you changed that file, and provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or placing the modifications on a major archive site such as ftp.uu.net, or by allowing the Copyright Holder to include your modifications in the Standard Version of the Package. b) use the modified Package only within your corporation or organization. c) rename any non-standard executables so the names do not conflict with standard executables, which must also be provided, and provide a separate manual page for each non-standard executable that clearly documents how it differs from the Standard Version. d) make other distribution arrangements with the Copyright Holder. 4. You may distribute the programs of this Package in object code or executable form, provided that you do at least ONE of the following: a) distribute a Standard Version of the executables and library files, together with instructions (in the manual page or equivalent) on where to get the Standard Version. b) accompany the distribution with the machine-readable source of the Package with your modifications. c) accompany any non-standard executables with their corresponding Standard Version executables, giving the non-standard executables non-standard names, and clearly documenting the differences in manual pages (or equivalent), together with instructions on where to get the Standard Version. d) make other distribution arrangements with the Copyright Holder. 5. You may charge a reasonable copying fee for any distribution of this Package. You may charge any fee you choose for support of this Package. You may not charge a fee for this Package itself. However, you may distribute this Package in aggregate with other (possibly commercial) programs as part of a larger (possibly commercial) software distribution provided that you do not advertise this Package as a product of your own. 6. The scripts and library files supplied as input to or produced as output from the programs of this Package do not automatically fall under the copyright of this Package, but belong to whomever generated them, and may be sold commercially, and may be aggregated with this Package. 7. C or perl subroutines supplied by you and linked into this Package shall not be considered part of this Package. 8. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. 9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. The End dist.ini100644001750001750 130613076472546 16077 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11name = Weasel abstract = PHP's Mink inspired multi-protocol web-testing library for Perl version = 0.11 author = Erik Huelsmann 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] [MetaJSON] [Prereqs] perl = 5.10.1 Moose = 0 Module::Runtime = 0 List::Util = 0 [Prereqs / TestRequires] Test::More = 0 [Prereqs / DevelopRequires] File::Find = 0 File::Util = 0 Perl::Critic = 0 Test::Pod::Coverage = 0 META.yml100644001750001750 127213076472546 15706 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11--- abstract: "PHP's Mink inspired multi-protocol web-testing library for Perl" author: - 'Erik Huelsmann ' build_requires: Test::More: '0' configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 0 generated_by: 'Dist::Zilla version 6.008, CPAN::Meta::Converter version 2.142690' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Weasel requires: List::Util: '0' Module::Runtime: '0' Moose: '0' perl: v5.10.1 resources: bugtracker: https://github.com/ehuelsmann/weasel/issues repository: https://github.com/ehuelsmann/weasel.git version: '0.11' x_serialization_backend: 'YAML::Tiny version 1.64' MANIFEST100644001750001750 114413076472546 15564 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.008. CHANGES LICENSE MANIFEST META.json META.yml Makefile.PL README README.md dist.ini lib/Weasel.pm lib/Weasel/DriverRole.pm lib/Weasel/Element.pm lib/Weasel/Element/Document.pm lib/Weasel/FindExpanders.pm lib/Weasel/FindExpanders/HTML.pm lib/Weasel/Session.pm lib/Weasel/WidgetHandlers.pm lib/Weasel/Widgets/HTML.pm lib/Weasel/Widgets/HTML/Button.pm lib/Weasel/Widgets/HTML/Input.pm lib/Weasel/Widgets/HTML/Select.pm lib/Weasel/Widgets/HTML/Selectable.pm t/00-load.t t/01-logging.t xt/01-critic.t xt/02-pod-coverage.t xt/perlcriticrc README.md100644001750001750 1037113076472546 15734 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11 # NAME Weasel - Perl's php/Mink-inspired abstracted web-driver framework # VERSION 0.01 # SYNOPSIS ```perl 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'); ``` # DESCRIPTION This module abstracts away the differences between the various web-driver protocols, like the Mink project does for PHP. While heavily inspired by Mink, `Weasel` aims to improve over it by being extensible, providing not just access to the underlying browser, yet to provide building blocks for further development and abstraction. [Pherkin::Extension::Weasel](https://github.com/perl-weasel/pherkin-extension-weasel) provides integration with [Test::BDD::Cucumber](https://github.com/pjlsergeant/test-bdd-cucumber-perl) (aka pherkin), for BDD testing. For the actual page interaction, this module needs a driver to be installed. Currently, that means [Weasel::Driver::Selenium2](https://github.com/perl-weasel/weasel-driver-selenium2). Other driver implementations, such as [Sahi](http://sahipro.com/) can be independently developed and uploaded to CPAN, or contributed. (We welcome and encourage both!) ## DIFFERENCES WITH OTHER FRAMEWORKS ### Mnemonics for element lookup patterns The central registry of xpath expressions to find common page elements helps to keep page access code clean. E.g. compare: ```perl use Weasel::FindExpanders::HTML; $session->page->find('*contains', text => 'Some text'); ``` With ```perl $session->page->find(".//*[contains(.,'Some text')] [not(.//*[contains(.,'Some text')])]"); ``` Multiple patterns can be registered for a single mnemonic. These which be concatenated into a single xpath expression. This concatenated expression allows to efficiently find matching elemnets with a single driver query. Besides good performance, this has the benefit that the following ```perl $session->page->find('*button', text => 'Click!'); ``` can be easily extended to match [Dojo toolkit's](http://dojotoolkit.org/documentation/) buttons as well as regular buttens. The problem with Dojo's buttons is that their DOM tree doesn't actually contain (visible) BUTTON or INPUT tags. To load support for Dojo widgets, simply: ```perl use Weasel::Widgets::Dojo; ``` ### Widgets encapsulate specific behaviours All elements in `Weasel` are of the base type `Weasel::Element`, which encapsulates the regular element interactions (click, find children, etc). While most elements will be represented by `Weasel::Element`, it's possible to implement other wrappers. These offer a logical extension point to implement tag-specific utility functions. E.g. `Weasel::Widgets::HTML::Select`, which adds the utility function `select_option`. These widgets also offer a good way to override default behaviours. One such case is the Dojo implementation of a `select` element. This element replaces the select tag entirely and in contrast with the original, doesn't keep the options as child elements of the `select`-replacing tag. By using the Dojo widget library ```perl use Weasel::Widget::Dojo; ``` the lack of the parent/child relation between the the select and its options is transparently handled by overriding the widget's `find` and `find_all` methods. # INSTALLATION ```sh # Install Weasel $ cpanm Weasel # Install Weasel's web driver $ cpanm Weasel::Driver::Selenium2 ``` If you want to use Weasel's support for Dojo-widget interaction, also: ```sh $ cpanm Weasel::Widgets::Dojo ``` If you want to use Weasel with its Pherkin (BDD) integration, also: ```sh $ cpanm Pherkin::Extension::Weasel ``` # SUPPORT ## BUGS Bugs can be filed in the GitHub issue tracker for the Weasel project: https://github.com/perl-weasel/weasel/issues ## DISCUSSION Community support is available through [perl-weasel@googlegroups.com](mailto:perl-weasel@googlegroups.com). Chat support is available in the [#perl-weasel:matrix.org](https://vector.im/beta/#/room/#perl-weasel:matrix.org) channel # COPYRIGHT ``` Copyright (c) 2016 Erik Huelsmann ``` # LICENSE Same as Perl META.json100644001750001750 265613076472546 16065 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11{ "abstract" : "PHP's Mink inspired multi-protocol web-testing library for Perl", "author" : [ "Erik Huelsmann " ], "dynamic_config" : 0, "generated_by" : "Dist::Zilla version 6.008, CPAN::Meta::Converter version 2.142690", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : "2" }, "name" : "Weasel", "prereqs" : { "configure" : { "requires" : { "ExtUtils::MakeMaker" : "0" } }, "develop" : { "requires" : { "File::Find" : "0", "File::Util" : "0", "Perl::Critic" : "0", "Test::Pod::Coverage" : "0" } }, "runtime" : { "requires" : { "List::Util" : "0", "Module::Runtime" : "0", "Moose" : "0", "perl" : "v5.10.1" } }, "test" : { "requires" : { "Test::More" : "0" } } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/ehuelsmann/weasel/issues" }, "repository" : { "type" : "git", "url" : "https://github.com/ehuelsmann/weasel.git", "web" : "https://github.com/ehuelsmann/weasel" } }, "version" : "0.11", "x_serialization_backend" : "JSON::XS version 3.03" } t000755001750001750 013076472546 14536 5ustar00ehuelsmannehuelsmann000000000000Weasel-0.1100-load.t100644001750001750 26213076472546 16177 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/t#!perl use strict; use warnings; use Test::More; use_ok($_) for (qw(Weasel Weasel::Session Weasel::Element Weasel::Element::Document Weasel::DriverRole)); done_testing; Makefile.PL100644001750001750 217213076472546 16407 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.008. use strict; use warnings; use 5.010001; use ExtUtils::MakeMaker; my %WriteMakefileArgs = ( "ABSTRACT" => "PHP's Mink inspired multi-protocol web-testing library for Perl", "AUTHOR" => "Erik Huelsmann ", "CONFIGURE_REQUIRES" => { "ExtUtils::MakeMaker" => 0 }, "DISTNAME" => "Weasel", "LICENSE" => "perl", "MIN_PERL_VERSION" => "5.010001", "NAME" => "Weasel", "PREREQ_PM" => { "List::Util" => 0, "Module::Runtime" => 0, "Moose" => 0 }, "TEST_REQUIRES" => { "Test::More" => 0 }, "VERSION" => "0.11", "test" => { "TESTS" => "t/*.t" } ); my %FallbackPrereqs = ( "List::Util" => 0, "Module::Runtime" => 0, "Moose" => 0, "Test::More" => 0 ); unless ( eval { ExtUtils::MakeMaker->VERSION(6.63_03) } ) { delete $WriteMakefileArgs{TEST_REQUIRES}; delete $WriteMakefileArgs{BUILD_REQUIRES}; $WriteMakefileArgs{PREREQ_PM} = \%FallbackPrereqs; } delete $WriteMakefileArgs{CONFIGURE_REQUIRES} unless eval { ExtUtils::MakeMaker->VERSION(6.52) }; WriteMakefile(%WriteMakefileArgs); lib000755001750001750 013076472546 15041 5ustar00ehuelsmannehuelsmann000000000000Weasel-0.11Weasel.pm100644001750001750 1567213076472546 17012 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib =head1 NAME Weasel - Perl's php/Mink-inspired abstracted web-driver framework =head1 VERSION 0.11 =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. While heavily inspired by Mink, C aims to improve over it by being extensible, providing not just access to the underlying browser, yet to provide building blocks for further development and abstraction. L provides integration with L (aka pherkin), for BDD testing. For the actual page interaction, this module needs a driver to be installed. Currently, that means L. Other driver implementations, such as L can be independently developed and uploaded to CPAN, or contributed. (We welcome and encourage both!) =head2 DIFFERENCES WITH OTHER FRAMEWORKS =over =item Mnemonics for element lookup patterns The central registry of xpath expressions to find common page elements helps to keep page access code clean. E.g. compare: use Weasel::FindExpanders::HTML; $session->page->find('*contains', text => 'Some text'); With $session->page->find(".//*[contains(.,'Some text')] [not(.//*[contains(.,'Some text')])]"); Multiple patterns can be registered for a single mnemonic, which will be concatenated to a single xpath expression to find the matching tags in a single driver query. Besides good performance, this has the benefit that the following $session->page->find('*button', text => 'Click!'); can be easily extended to match L buttons, which on the HTML level don't contain visible button or input tags, simply by using the widget support set: use Weasel::Widgets::Dojo; =item Widgets encapsulate specific behaviours All elements in C are of the base type C, which encapsulates the regular element interactions (click, find children, etc). While most elements will be represented by C, it's possible to implement other wrappers. These offer a logical extension point to implement tag-specific utility functions. E.g. C, which adds the utility function C. These widgets also offer a good way to override default behaviours. One such case is the Dojo implementation of a 'select' element. This element replaces the select tag entirely and in contrast with the original, doesn't keep the options as child elements of the 'select'-replacing tag. By using the Dojo widget library use Weasel::Widget::Dojo; the lack of the parent/child relation between the the select and its options is transparently handled by overriding the widget's C and C methods. =back =cut package Weasel; use strict; use warnings; use Moose; our $VERSION = '0.11'; # From https://w3c.github.io/webdriver/webdriver-spec.html#keyboard-actions my %key_codes = ( NULL => "\N{U+E000}", CANCEL => "\N{U+E001}", HELP => "\N{U+E002}", BACK_SPACE => "\N{U+E003}", TAB => "\N{U+E004}", CLEAR => "\N{U+E005}", RETURN => "\N{U+E006}", ENTER => "\N{U+E007}", SHIFT => "\N{U+E008}", CONTROL => "\N{U+E009}", ALT => "\N{U+E00A}", PAUSE => "\N{U+E00B}", ESCAPE => "\N{U+E00C}", SPACE => "\N{U+E00D}", PAGE_UP => "\N{U+E00E}", PAGE_DOWN => "\N{U+E00F}", 'END' => "\N{U+E010}", HOME => "\N{U+E011}", ARROW_LEFT => "\N{U+E012}", ARROW_UP => "\N{U+E013}", ARROW_RIGHT => "\N{U+E014}", ARROW_DOWN => "\N{U+E015}", INSERT => "\N{U+E016}", DELETE => "\N{U+E017}", SEMICOLON => "\N{U+E018}", EQUALS => "\N{U+E019}", NUMPAD0 => "\N{U+E01A}", NUMPAD1 => "\N{U+E01B}", NUMPAD2 => "\N{U+E01C}", NUMPAD3 => "\N{U+E01D}", NUMPAD4 => "\N{U+E01E}", NUMPAD5 => "\N{U+E01F}", NUMPAD6 => "\N{U+E020}", NUMPAD7 => "\N{U+E021}", NUMPAD8 => "\N{U+E022}", NUMPAD9 => "\N{U+E023}", MULTIPLY => "\N{U+E024}", ADD => "\N{U+E025}", SEPARATOR => "\N{U+E026}", SUBTRACT => "\N{U+E027}", DECIMAL => "\N{U+E028}", DIVIDE => "\N{U+E029}", F1 => "\N{U+E031}", F2 => "\N{U+E032}", F3 => "\N{U+E033}", F4 => "\N{U+E034}", F5 => "\N{U+E035}", F6 => "\N{U+E036}", F7 => "\N{U+E037}", F8 => "\N{U+E038}", F9 => "\N{U+E039}", F10 => "\N{U+E03A}", F11 => "\N{U+E03B}", F12 => "\N{U+E03C}", META => "\N{U+E03D}", COMMAND => "\N{U+E03D}", ZENKAKU_HANKAKU => "\N{U+E040}", ); =item KEYS Returns a reference to a hash with names of the keys in the hash keys and single-character strings containing the key codes as the values. =cut sub KEYS { return \%key_codes; } =head1 ATTRIBUTES =over =item default_session The name of the default session to return from C, 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 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. =head1 COPYRIGHT (C) 2016 Erik Huelsmann Licensed under the same terms as Perl. =cut 1; xt000755001750001750 013076472546 14726 5ustar00ehuelsmannehuelsmann000000000000Weasel-0.1101-critic.t100644001750001750 163713076472546 16755 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/xt#!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 => '', -exclude => [ 'OTRS::', # some CPANtesters use this by default ], -include => [ # 'Documentation::' ]), \@on_disk); done_testing; 01-logging.t100644001750001750 411713076472546 16732 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/t#!perl use Data::Dumper; use Test::More; package DummyDriver; use Data::Dumper; use Moose; with 'Weasel::DriverRole'; sub implements { return $Weasel::DriverRole::VERSION; } sub tag_name { my ($self, $tag) = @_; return $tag->{tag}; } sub find_all { my @rv = ( { tag => 'span' }, { tag => 'span' }, ); return (wantarray) ? @rv : \@rv; } package main; use Weasel; use Weasel::Session; my @logs; my $weasel = Weasel->new( default_session => 'default', sessions => { default => Weasel::Session->new( driver => DummyDriver->new(), log_hook => sub { my ($event, $item) = @_; $item = $item->() if ref $item eq 'CODE'; push @logs, [ $event, $item ]; }, ), }, ); my $session = $weasel->session; # Specifically test `find_all' due to the complex nature: # It can return an array ref in scalar context or an array in # list context -- yet the logger will receive an array ref (always) my @found = $session->page->find_all('span'); my $found = $session->page->find_all('span'); is(scalar(@found), 2, 'Number of tags found equals two'); is(ref $found, 'ARRAY', 'Scalar context returns ARRAYREF'); is_deeply(\@logs, [['pre_find_all', 'pattern: span'], ['pre_tag_name', 'getting tag name'], ['post_tag_name', 'found tag with name span'], ['pre_tag_name', 'getting tag name'], ['post_tag_name', 'found tag with name span'], ['post_find_all', 'found 2 elements for span - Weasel::Element (span) - Weasel::Element (span)'], ['pre_find_all', 'pattern: span'], ['pre_tag_name', 'getting tag name'], ['post_tag_name', 'found tag with name span'], ['pre_tag_name', 'getting tag name'], ['post_tag_name', 'found tag with name span'], ['post_find_all', 'found 2 elements for span - Weasel::Element (span) - Weasel::Element (span)'] ], 'Compare log output'); done_testing; perlcriticrc100644001750001750 10313076472546 17450 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/xt[Modules::ProhibitEvilModules] modules = Carp::Always Data::Dumper 02-pod-coverage.t100644001750001750 66313076472546 20032 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/xt#!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; Weasel000755001750001750 013076472546 16261 5ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/libSession.pm100644001750001750 2712113076472546 20425 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel =head1 NAME Weasel::Session - Connection to an encapsulated test driver =head1 VERSION 0.02 =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 Module::Runtime qw/ use_module /;; use Weasel::FindExpanders qw/ expand_finder_pattern /; use Weasel::WidgetHandlers qw| best_match_handler_class |; our $VERSION = '0.02'; =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 used with the session, or uses all groups when undefined. Note: this functionality allows to load multiple groups into the running perl instance, while using different groups in various sessions. =cut has 'widget_groups' => (is => 'rw'); =item base_url Holds the prefix that will be prepended to every URL passed to this API. The prefix can be an environment variable, e.g. ${VARIABLE}. It will be expanded and default to hppt://localhost:5000 if not defined. If it is not an environment variable, it will be used as is. =cut has 'base_url' => (is => 'rw', isa => 'Str', default => '' ); =item page Holds the root element of the target HTML page (the 'html' tag). =cut has 'page' => (is => 'ro', isa => 'Weasel::Element::Document', builder => '_page_builder', lazy => 1); sub _page_builder { my $self = shift; my $class = use_module($self->page_class); return $class->new(session => $self); } =item log_hook Upon instantiation can be set to log consumer; a function of 3 arguments: 1. the name of the event 2. the text to be logged (or a coderef to be called without arguments returning such) =cut has 'log_hook' => (is => 'ro', isa => 'Maybe[CodeRef]'); =item page_class Upon instantiation can be set to an alternative class name for the C attribute. =cut has 'page_class' => (is => 'ro', isa => 'Str', default => 'Weasel::Element::Document'); =item retry_timeout The number of seconds to poll for a condition to become true. Global setting for the C function. =cut has 'retry_timeout' => (is => 'rw', default => 15, isa => 'Num', ); =item poll_delay The number of seconds to wait between state polling attempts. Global setting for the C function. =cut has 'poll_delay' => (is => 'rw', default => 0.5, isa => 'Num', ); =back =head1 METHODS =over =item clear($element) Clears any input entered into elements supporting it. Generally applies to textarea elements and input elements of type text and password. =cut sub clear { my ($self, $element) = @_; $self->_logged(sub { $self->driver->clear($element->_id); }, 'clear', 'clearing input element'); } =item click([$element]) Simulates a single mouse click. If an element argument is provided, that element is clicked. Otherwise, the browser window is clicked at the current mouse location. =cut sub click { my ($self, $element) = @_; $self->_logged( sub { $self->driver->click(($element) ? $element->_id : undef); }, 'click', ($element) ? 'clicking element' : 'clicking window'); } =item find($element, $locator [, scheme => $scheme] [, %locator_args]) Finds the first child of C<$element> matching C<$locator>. See L's C function for more documentation. =cut sub find { my ($self, @args) = @_; my $rv; $self->_logged( sub { $self->wait_for( sub { my @rv = @{$self->find_all(@args)}; return $rv = shift @rv; }); }, 'find', 'find ' . $args[1]); return $rv; } =item find_all($element, $locator, [, scheme => $scheme] [, %locator_args ]) Finds all child elements of C<$element> matching C<$locator>. Returns, depending on scalar or list context, an arrayref or a list with matching elements. See L's C function for more documentation. =cut sub find_all { my ($self, $element, $pattern, %args) = @_; my $expanded_pattern = expand_finder_pattern($pattern, \%args); my @rv = $self->_logged( sub { return map { $self->_wrap_widget($_) } $self->driver->find_all($element->_id, $expanded_pattern, $args{scheme}); }, 'find_all', sub { my ($rv) = @_; return "found " . scalar(@$rv) . " elements for $pattern " . (join(', ', %args)) . "\n" . (join("\n", map { ' - ' . ref($_) . ' (' . $_->tag_name . ")" } @$rv)); }, "pattern: $pattern"); return wantarray ? @rv : \@rv; } =item get($url) Loads C<$url> into the active browser window of the driver connection, after prefixing with C. =cut sub get { my ($self, $url) = @_; my $base = $self->base_url =~ /\$\{([a-zA-Z0-9_]+)\}/ ? $ENV{$1} // "http://localhost:5000" : $self->base_url; $url = $base . $url; ###TODO add logging warning of urls without protocol part # which might indicate empty 'base_url' where one is assumed to be set $self->_logged( sub { return $self->driver->get($url); }, 'get', "loading URL: $url"); } =item get_attribute($element, $attribute) Returns the value of the attribute named by C<$attribute> of the element identified by C<$element>, or C if the attribute isn't defined. =cut sub get_attribute { my ($self, $element, $attribute) = @_; return $self->_logged( sub { return $self->driver->get_attribute($element->_id, $attribute); }, 'get_attribute', "element attribute '$attribute'"); } =item get_text($element) Returns the 'innerHTML' of the element identified by C<$element>. =cut sub get_text { my ($self, $element) = @_; return $self->_logged( sub { return $self->driver->get_text($element->_id); }, 'get_text', 'element text'); } =item is_displayed($element) Returns a boolean value indicating if the element identified by C<$element> is visible on the page, i.e. that it can be scrolled into the viewport for interaction. =cut sub is_displayed { my ($self, $element) = @_; return $self->_logged( sub { return $self->driver->is_displayed($element->_id); }, 'is_displayed', 'query is_displayed'); } =item screenshot($fh) Writes a screenshot of the browser's window to the filehandle C<$fh>. Note: this version assumes pictures of type PNG will be written; later versions may provide a means to query the exact image type of screenshots being generated. =cut sub screenshot { my ($self, $fh) = @_; $self->_logged( sub { $self->driver->screenshot($fh); }, 'screenshot', 'screenshot'); } =item get_page_source($fh) Writes a get_page_source of the browser's window to the filehandle C<$fh>. =cut sub get_page_source { my ($self) = @_; $self->_logged( sub { $self->driver->get_page_source(); }, 'get_page_source', 'get_page_source'); } =item send_keys($element, @keys) Send the characters specified in the strings in C<@keys> to C<$element>, simulating keyboard input. =cut sub send_keys { my ($self, $element, @keys) = @_; $self->_logged( sub { $self->driver->send_keys($element->_id, @keys); }, 'send_keys', 'sending keys: ' . join('', @keys // ())); } =item tag_name($element) Returns the tag name of the element identified by C<$element>. =cut sub tag_name { my ($self, $element) = @_; return $self->_logged(sub { return $self->driver->tag_name($element->_id) }, 'tag_name', sub { my $tag = shift; return "found tag with name $tag" }, 'getting tag name'); } =item wait_for($callback, [ retry_timeout => $number,] [poll_delay => $number]) Polls $callback->() until it returns true, or C expires -- whichever comes first. The arguments retry_timeout and poll_delay can be used to override the session-global settings. =cut sub wait_for { my ($self, $callback, %args) = @_; $self->_logged( sub { $self->driver->wait_for($callback, retry_timeout => $self->retry_timeout, poll_delay => $self->poll_delay, %args); }, 'wait_for', 'waiting for condition'); } before 'BUILDARGS', sub { my ($class, @args) = @_; my $args = (ref $args[0]) ? $args[0] : { @args }; confess "Driver used to construct session object uses old API version; some functionality may not work correctly" if ($args->{driver} && $args->{driver}->implements < $Weasel::DriverRole::VERSION); }; sub _appending_wrap { my ($str) = @_; return sub { my $rv = shift; if ($rv) { return "$str ($rv)"; } else { return $str; } } } =item _logged($wrapped_fn, $event, $log_item, $log_item_pre) Invokes C when it's defined, before and after calling C<$wrapped_fn> with no arguments, with the 'pre_' and 'post_' prefixes to the event name. C<$log_item> can be a fixed string or a function of one argument returning the string to be logged. The argument passed into the function is the value returned by the C<$wrapped_fn>. In case there is no C<$log_item_pre> to be called on the 'pre_' event, C<$log_item> will be used instead, with no arguments. For performance reasons, the C<$log_item> and C<$log_item_pre> - when coderefs - aren't called; instead they are passed as-is to the C<$log_hook> for lazy evaluation. =cut sub _logged { my ($self, $f, $e, $l, $lp) = @_; my $hook = $self->log_hook; return $f->() if ! defined $hook; $lp //= $l; my $pre = (ref $lp eq 'CODE') ? $lp : _appending_wrap($lp); my $post = (ref $l eq 'CODE') ? $l : _appending_wrap($l); $hook->("pre_$e", $pre); if (wantarray) { my @rv = $f->(); $hook->("post_$e", sub { return $l->(\@rv); }); return @rv; } else { my $rv = $f->(); $hook->("post_$e", sub { return $l->($rv); }); return $rv; } }; =item _wrap_widget($_id) Finds all matching widget selectors to wrap the driver element in. In case of multiple matches, selects the most specific match (the one with the highest number of requirements). =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); } =back =head1 SEE ALSO L =head1 COPYRIGHT (C) 2016 Erik Huelsmann Licensed under the same terms as Perl. =cut 1; Element.pm100644001750001750 752313076472546 20357 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel =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 This module provides the base class for all page elements, encapsulating the regular element interactions, such as finding child element, querying attributes and the tag name, etc. =cut package Weasel::Element; use strict; use warnings; use Moose; =head1 ATTRIBUTES =over =item session Required. Holds a reference to the L to which the element belongs. Used to access the session's driver to query element properties.x =cut has session => (is => 'ro', isa => 'Weasel::Session', required => 1); =item _id Required. Holds the I used by the session's driver to identify the element. =cut has _id => (is => 'ro', required => 1); =back =head1 METHODS =over =item find($locator [, scheme => $scheme] [, %locator_args]) Finds the first child element matching c<$locator>. Returns C when not found. Optionally takes a scheme argument to identify non-xpath type locators. In case the C<$locator> is a mnemonic (starts with an asterisk ['*']), additional arguments may be provided for expansion of the mnemonic. See L for documentation of the standard expanders. =cut sub find { my ($self, @args) = @_; return $self->session->find($self, @args); } =item find_all($locator [, scheme => $scheme] [, %locator_args]) Returns, depending on scalar vs array context, a list or an arrayref with matching elements. Returns an empty list or ref to an empty array when none found. Optionally takes a scheme argument to identify non-xpath type locators. In case the C<$locator> is a mnemonic (starts with an asterisk ['*']), additional arguments may be provided for expansion of the mnemonic. See L for documentation of the standard expanders. =cut sub find_all { my ($self, @args) = @_; # expand $locator based on framework plugins (e.g. Dojo) return $self->session->find_all($self, @args); } =item get_attribute($attribute) Returns the value of the element's attribute named in C<$attribute> or C if none exists. Note: Some browsers apply default values to attributes which are not part of the original page. As such, there's no direct relation between the existence of attributes in the original page and this function returning C. =cut sub get_attribute { my ($self, $attribute) = @_; return $self->session->get_attribute($self, $attribute); } =item get_text() Returns the element's 'innerHTML'. =cut sub get_text { my ($self) = @_; return $self->session->get_text($self); } =item has_class =cut sub has_class { my ($self, $class) = @_; return grep { $_ eq $class } split /\s+/, ($self->get_attribute('class') // ''); } =item is_displayed Returns a boolean indicating if an element is visible (e.g. can potentially be scrolled into the viewport for interaction). =cut sub is_displayed { my ($self) = @_; return $self->session->is_displayed($self); } =item click() Scrolls the element into the viewport and simulates it being clicked on. =cut sub click { my ($self) = @_; $self->session->click($self); } =item send_keys(@keys) Focusses the element and simulates keyboard input. C<@keys> can be any number of strings containing unicode characters to be sent. E.g. $element->send_keys("hello", ' ', "world"); =cut sub send_keys { my ($self, @keys) = @_; $self->session->send_keys($self, @keys); } =item tag_name() Returns the name of the tag of the element, e.g. 'div' or 'input'. =cut sub tag_name { my ($self) = @_; return $self->session->tag_name($self); } =back =cut 1; DriverRole.pm100644001750001750 1553013076472546 21060 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel =head1 NAME Weasel::DriverRole - API definition for driver wrappers =head1 VERSION 0.02 =head1 SYNOPSIS use Moose; use Weasel::DriverRole; with 'Weasel::DriverRole'; ... # (re)implement the functions in Weasel::DriverRole =head1 DESCRIPTION This module defines the API for all Weasel drivers to be implemented. By using this role in the driver implementation module, an abstract method is implmented croak()ing if it's called. =cut package Weasel::DriverRole; use strict; use warnings; use Carp; use Moose::Role; our $VERSION = '0.02'; =head1 ATTRIBUTES =over =item started Every session is associated with a driver instance. The C attribute holds a boolean value indicating whether or not the driver is ready to receive driver commands. The value managed by the C and C methods. =cut has 'started' => (is => 'rw', isa => 'Bool', default => 0); =back =head1 METHODS =over =item implements This method returns the version number of the API which it fully implements. L may carp (warn) the user about mismatching API levels in case a driver is coded against an earlier version than C<$Weasel::DriverRole::VERSION>. =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 This method allows setup of the driver. It is invoked before any web driver methods as per the Web driver methods section below. =cut sub start { my $self = shift; $self->started(1); } =item stop This method allows tear-down of the driver. After tear-down, the C method may be called again, so the this function should leave the driver in a restartable state. =cut sub stop { my $self = shift; $self->started(0); } =item restart This function stops (if started) and starts the driver. =cut sub restart { my $self = shift; $self->stop; $self->start; } =back =head2 Web driver methods =head3 Terms =over =item element_id / parent_id These are opaque values used by the driver to identify DOM elements. Note: The driver should always accept an xpath locator as an id value as well as id values returned from earlier driver calls =back =head3 API =over =item find_all( $parent_id, $locator, $scheme ) Returns the _id values for the elements to be instanciated, matching the C<$locator> using C. Depending on array or scalar context, the return value is a list or an arrayref. Note: there's no function to find a single element. That function is implemented on the C level. =cut sub find_all { croak "Abstract inteface method 'find_all' called"; } =item get( $url ) Loads the page at C<$url> into the driver's browser (browser emulator). The C<$url> passed in has been expanded by C, prepending a registered prefix. =cut sub get { croak "Abstract interface method 'get' called"; } =item is_displayed($element_id) Returns a boolean value indicating whether the element indicated by C<$element_id> is interactable (can be selected, clicked on, etc) =cut sub is_displayed { croak "Abstract interface method 'is_displayed' called"; } =item wait_for( $callback, retry_timeout => $num, poll_delay => $num ) The driver may interpret the 'poll_delay' in one of two ways: 1. The 'poll_delay' equals the number of seconds between the start of successive poll requests 2. The 'poll_delay' equals the number of seconds to wait between the end of one poll request and the start of the next Note: The user should catch inside the callback any exceptions that are thrown inside the callback, unless such exceptions are allowed to terminate further polling attempts. I.e. this function doesn't guard against early termination by catching exceptions. =cut sub wait_for { croak "Abstract interface method 'wait_for' called"; } =item clear($element_id) Clicks on an element if an element id is provided, or on the current mouse location otherwise. =cut sub clear { croak "Abstract interface method 'clear' 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) Returns the value of the attribute named by C<$attribute_name> of the element indicated by C<$element_id>. =cut sub get_attribute { croak "Abstract interface method 'get_attribute' called"; } =item get_page_source($fh) Writes a get_page_source of the browser's window to the filehandle C<$fh>. =cut sub get_page_source { croak "Abstract interface method 'get_page_source' called"; } =item get_text($element_id) Returns the HTML content of the element identified by C<$element_id>, the so-called 'innerHTML'. =cut sub get_text { croak "Abstract interface method 'get_text' called"; } =item set_attribute($element_id, $attribute_name, $value) Changes the value of the attribute named by C<$attribute_name> to C<$value> for the element identified by C<$element_id>. =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"; } =item screenshot($fh) Takes a screenshot and writes the image to the file handle C<$fh>. Note: In the current version of the driver, it's assumed the driver writes a PNG image. Later versions may add APIs to get/set the type of image generated. =cut sub screenshot { croak "Abstract interface method 'screenshot' called"; } =item send_keys($element_id, @keys) Simulates key input into the element identified by C<$element_id>. C<@keys> is an array of (groups of) inputs; multiple multi-character strings may be listed. In such cases the input will be appended. E.g. $driver->send_keys($element_id, "hello", ' ', "world"); is valid input to enter the text "hello world" into C<$element_id>. Note: Special keys are encoded according to the WebDriver spec. In case a driver implementation needs differentt encoding of special keys, this function should recode from the values found in WebDriver::KEYS() to the desired code-set =cut sub send_keys { croak "Abstract interface method 'send_keys' called"; } =item tag_name($element_id) The name of the HTML tag identified by C<$element_id>. =cut sub tag_name { croak "Abstract interface method 'tag_name' called"; } =back =head1 SEE ALSO L =head1 COPYRIGHT (C) 2016 Erik Huelsmann Licensed under the same terms as Perl. =cut 1; Widgets000755001750001750 013076472546 17667 5ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/WeaselHTML.pm100644001750001750 124513076472546 21133 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel/Widgets =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, use Weasel::Widgets::HTML::Select; # No widgets for file inputs and # more importantly TEXTAREA, FORM and SELECT 1; FindExpanders.pm100644001750001750 422613076472546 21515 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel =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'; use Carp; 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 selects the expanders to be applied using its C 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, $args, $groups) Returns a string of concatenated (using xpath '|' operator) expansions. When C<$groups> is undef, all groups will be searched for C. If the pattern doesn't match '*|{}', the pattern is returned as the only list/arrayref element. =cut sub expand_finder_pattern { my ($pattern, $args, $groups) = @_; return $pattern if ! ($pattern =~ m/^\*([^\|]+)/); my $name = $1; croak "No expansions registered (while expanding '$pattern')" if scalar(keys %find_expanders) == 0; $groups //= [ keys %find_expanders ]; # undef --> unrestricted # Using eval below to transform a hash-in-string to a hash efficiently my @matches; for my $group (@$groups) { next if ! exists $find_expanders{$group}{$name}; push @matches, reverse map { $_->(%$args) } @{$find_expanders{$group}{$name}}; } croak "No expansions matching '$pattern'" if ! @matches; return join "\n|", @matches; } =back =cut 1; WidgetHandlers.pm100644001750001750 772613076472546 21677 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel =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 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 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, $att)); } sub _att_eq { my ($att1, $att2) = @_; return ($att1 // '') eq ($att2 // ''); } 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 _att_eq( $conditions->{attributes}->{$att}, _cached_elem_att( $elem_att_cache, $driver, $_id, $att)); $match_count++; } push @matches, { count => $match_count, class => $handler->{class}, }; } } 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; Element000755001750001750 013076472546 17652 5ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/WeaselDocument.pm100644001750001750 52213076472546 22105 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel/Element =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; FindExpanders000755001750001750 013076472546 21013 5ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/WeaselHTML.pm100644001750001750 1245113076472546 22300 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel/FindExpanders =head1 NAME Weasel::FindExpanders::HTML - =head1 VERSION 0.02 =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 contains_expander Finds tags containing 'text' =cut sub contains_expander { my %args = @_; my $text = $args{text}; return ".//*[contains(.,'$text')][not(.//*[contains(.,'$text')])]"; } =item labeled_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 labeled_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 $args{$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 => 'contains', expander => \&contains_expander }, { name => 'labeled', expander => \&labeled_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; HTML000755001750001750 013076472546 20433 5ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel/WidgetsInput.pm100644001750001750 211013076472546 22222 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel/Widgets/HTML =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 clear() =cut sub clear { my ($self) = @_; $self->session->clear($self); } =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; Button.pm100644001750001750 126013076472546 22403 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel/Widgets/HTML =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; Select.pm100644001750001750 143313076472546 22351 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel/Widgets/HTML =head1 NAME Weasel::Widgets::HTML::Select - Wrapper of SELECT tag =head1 VERSION 0.01 =head1 SYNOPSIS =head1 DESCRIPTION =cut package Weasel::Widgets::HTML::Select; 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 => 'select', ); =head1 METHODS =over =item find_option() Returns =cut # sub _option_popup { my ($self) = @_; return $self; } sub find_option { my ($self, $text) = @_; my $popup = $self->_option_popup; return $popup->find('*option', text => $text); } =item select_option =cut sub select_option { my ($self, $text) = @_; $self->find_option($text)->click; } 1; Selectable.pm100644001750001750 267013076472546 23201 0ustar00ehuelsmannehuelsmann000000000000Weasel-0.11/lib/Weasel/Widgets/HTML =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_attribute($self, 'selected', $value) if defined $value; return $self->session->get_attribute($self, 'selected'); } =item get_attribute($name) Returns the value of the attribute; when the element is I selected, the 'value' attribute is overruled to return C (an empty string). =cut sub get_attribute { my ($self, $name) = @_; if ($name eq 'value' && ! $self->selected) { return ''; # false/ not selected } else { return $self->SUPER::get_attribute($name); } } =back =cut 1;