Manage the browser with PHP and Selenium

Intro


 
Hello! Today I will tell you about how you can work with PHP with Selenium .
 
 
Most often, this is necessary when you are faced with the task of writing autotests for the web interface or your own parser /crawler.
 
 
From Wikipedia [/b]
"Selenium is a tool for automating the actions of a web browser.
 
In most cases, it is used to test Web applications, but this is not
 
is limited. In particular, the implementation of Selenium WebDriver for the browser phantomjs
 
often used as a web grabber. "

 
 
We will consider the following nuances:
 
 
 
Use of Behat / Mink for connection with Selenium
 
 
Running Selenium in the docker, and remote access on VNC
 
 
We extend the functionality of Behat with Extension Feature
 
 
product. Cucumber , which is often used in other programming languages.
 
 
By itself, it is not able to work with Selenium and is intended more for writing functional tests without using a browser.
 
 
It is installed as a normal package:
 
 
$ composer require "behat /behat"
 
To teach behat to work with the browser, we will need its extension Mink, as well as the bridge to work with a specific vendor (in our case it's Selenium). You can find the full list of vendors on page Mink . With the versions, your composer.json should look something like this:
 

 
    "require": {
"behat /behat": "^ 3.4",
"behat /mink-extension": "2.2",
"behat /mink-selenium2-driver": "^ 1.3"
}

 
After installation, you will have vendor /bin /behat the file responsible for running the tests. If vendor /bin /behat --version showed you the installed version, then with a high degree of probability the installation was successful :)
 

 
The final phase is the
configuration.  

 
Create the main configuration file behat.yml in the project root [/b]
    default:
# Specify the path for the autoloader to "context" for classes
autoload:
'': '% paths.base% /src /Context'
suites:
# We announce the test of the suite
facebook_suite:
# path (s) to scripting files written in Gherkin language
paths:
- '% paths.base% /scenario /facebook'
contexts:
# We fix a certain "context" class behind the suite.
# The class API is available in the
script. - DossierContextFacebookContext:
# Optionally pass the parameters to the constructor of the class FacebookContext
base_url: 'https://www.facebook.com/'
user: '[email protected]'
pass: 'password'
vk_suite:
paths:
- '% paths.base% /scenario /vk'
contexts:
- DossierContextVkContext:
# Here we pass the class instance as the
dependency. - "@lookup"
services:
# mapp alias to the service class
lookup: 'DossierContextAccessLimitLookup'
extensions:
# Declare the list of extensions used by behat
BehatMinkExtension:
browser_name: 'chrome'
default_session: 'selenium2'
selenium2:
# Selenium server address. In this case, the standard IP docker (in your case there may be a localhost or a remote server)
wd_host: ' http://???.1:4444/wd/hub '
The default browser is
browser: chrome

 

 
Scripts files or (* .feature files) are yml files written in pseudo-language Gherkin , contain, in fact, a set of step-by-step instructions that your browser will execute during the execution of a specific suite. More information about the syntax you can learn by clicking on the link above.
 

 
Each such "instruction" in turn is matched to the methods of the "context" class using of the regular expressions specified in class annotations. BehatMinkExtensionContextMinkContext
 
The names of the methods of the role themselves do not play, although it will be a good idea to follow the naming convention in CamelCase.
 

 
If you do not have enough of the default Gherkin constructs, you can extend the functionality in the classes of the MinkContext heirs by correctly specifying the annotations. This role is played by the "context" classes.
 

 

2. Installation and configuration of the environment


 
Those of you who have already worked with Selenium know that after starting the test, the browser will run on the machine and the steps specified in the .feature file will pass.
 

 
Running Selenium in Docker is a bit more complicated. Firstly, you need Ixs in the container, and secondly, you will want to see what happens inside the container.
 

 
The guys from Selenium are already all about took care and you do not have to collect your container. A container with Standalone server on board will be immediately available on the 5900 port, where you can knock from any VNC client (for example, with this ). Inside the container you will be greeted by a friendly Fluxbox interface with Chrome preinstalled. In my case it looks like this:
 

 
Manage the browser with PHP and Selenium
 

 
To come to success, you can start the docker container, according to the instructions on the website:
 

 
    $ docker run -d -p 4444: 4444 -p 5900: 5900 -v /dev /shm: /dev /shm selenium /standalone-chrome-debug: ???-californium    

 
The important point, without the Shore Volumn /dev /shm chrome does not have enough memory and it can not start, so do not forget to specify it.
 

 
In my case, is used. docker-compose , and the YAML file will look like this:
 

 
    version: '2'
services:
selenium:
image: selenium /standalone-chrome-debug: ???
ports:
- "4444: 4444"
- "5900: 5900"
volumes:
- /dev /shm: /dev /shm
network_mode: "host"

 
I want my tests to go to Facebook via the VPN included on the host machine, so it's important to specify network_mode .
 

 
To start the container using compose, run the following command:
 

 
    $ docker-compose up    

 
Now we try to connect via VNC to localhost: 5900 and open the browser inside the container. If you succeeded and you see something similar to the screenshot above - you have passed this level.
 

 

3. From theory to practice. Automate


 
In the example below, I'll get all Facebook users on the given name and surname. The script will look like this:
 

 
src /scenario /facebook /facebook.feature [/b]
    Feature: Facebook Parse
In order parse fb
@ first-level
Scenario: Find a person in facebook
Given I am on "https://facebook.com/"
When I fill in "email" with "[email protected]"
And I fill in "pass" with "somepass"
# Custom instruction
Then I press tricky facebook login button
Then I should see "Search"
# Custom instructions
Then I am looking for input params
Then I dump users

 

 
And, accordingly, the Context class (the constructor and the namespace are omitted)
 

 
src /Context /FacebookContext.php [/b]
    class FacebookContext extends MainContext
{
/**
* @Then /^ I press tricky facebook login button $ /
* /
public function pressFacebookButton ()
{
$ this-> getSession () -> getPage () -> find (
'css',
'input[data-testid="royal_login_button"]'
) -> click ();
}
/**
* We collect the information that interests me. Avatar, links, add. information
* @Then /^ I dump users $ /
* /
public function dumpUsers ()
{
$ session = $ this-> getSession ();
$ users = $ this-> getSession () -> getPage () -> findAll (
'xpath',
$ session-> getSelectorsHandler ()
-> selectorToXpath ('css', 'div._4p2o')
);
if (! $ users) {
throw new InvalidArgumentException ("The user with this name was not found");
}
$ collection = new UserCollection ('facebook_suite');
foreach ($ users as $ user) {
$ img = $ user-> find ('xpath', $ session-> getSelectorsHandler ()
-> selectorToXpath (
'xpath',
$ session-> getSelectorsHandler () -> selectorToXpath ('css',' img ')
));
$ link = $ user-> find ('xpath', $ session-> getSelectorsHandler ()
-> selectorToXpath (
'xpath',
$ session-> getSelectorsHandler () -> selectorToXpath ('css',' a._32mo ')
));
$ outputInfo = new OutputUserInfo ('facebook_suite');
$ outputInfo-> setName ($ link? $ link-> getText (): '')
-> addPublicLinks ($ link? $ link-> getAttribute ('href'): '')
-> setPhoto ($ img? $ img-> getAttribute ('src'): '');
$ collection-> append ($ outputInfo);
}
$ this-> saveDump ($ collection);
}
/**
* Get a search query and substitute it in the URL
* @Then /^ I am searching for input params $ /
* /
public function search ()
{
if (! Registry :: has ('query')) {
throw new BadMethodCallException ('No search query received');
}
$ criteria = Registry :: get ('query');
$ this-> getSession () -> visit ("https://www.facebook.com/search/people/?q=". urldecode ($ criteria-> getQuery ()));
}
}

 

 
Often there is a need for custom methods like FacebookContext :: pressFacebookButton, because by default all selectors in mink can search only by name | value | id | alt | title.
 

 
If you need a sample for another attribute, you'll have to write your own method. The login button on Facebook has the id attribute, but changes its value periodically for some reason. Therefore, I had to reconnect to the data-testid, which, for now, remains static.
 

 
Now,so that it all starts, you need to make sure that Selenium is running and listening to the specified port.
 

 
Then we execute:
 

 
    $ vendor /bin /behat    

 
Inside the container, the browser instance should start and follow the specified instructions.
 

 

4. Customization behat. Extensions


 
The Behat framework has an excellent extension mechanism through behat.yml . Note that many classes of the framework are declared as final to temper the temptation to simply inherit them.
 

 
The extension allows you to complement the behat function, declare new console arguments and options, modify the behavior of other extensions, etc. It consists of a class implementing
 
BehatTestworkServiceContainerExntension interface (it is also specified in behat.yml) and auxiliary classes, if necessary.
 

 
I want to teach behat to accept the name of the person sought through the new incoming argument --search-by-fullname , in order to subsequently use these data inside the suite.
 

 
Below I quote the code that performs the necessary operations:
 

 
SearchExtension [/b]
    use BehatBehatGherkinServiceContainerGherkinExtension;
use BehatTestworkCliServiceContainerCliExtension;
use BehatTestworkServiceContainerExtension;
use BehatTestworkServiceContainerExtensionManager;
use SymfonyComponentConfigDefinitionBuilderArrayNodeDefinition;
use SymfonyComponentDependencyInjectionContainerBuilder;
use SymfonyComponentDependencyInjectionDefinition;
use SymfonyComponentDependencyInjectionReference;
class SearchExtension implements Extension
{
/**
* Here you can modify the container with all Extensions before the output of
* /
public function process (ContainerBuilder $ container) {}
/**
* Unique prefix for the configuration of expansion in behat.yml
* @return string
* /
public function getConfigKey ()
{
return 'search';
}
/**
* This method is called immediately after the activation of all extensions, but
* before calling the configure () method. This allows extensions
* wedge in the configuration of other extensions
* @param ExtensionManager $ extensionManager
* /
public function initialize (ExtensionManager $ extensionManager) {}
/**
* Install additional configuration of the extension
* @param ArrayNodeDefinition $ builder
* /
public function configure (ArrayNodeDefinition $ builder) {}
/**
* Loads extension services into container
* @param ContainerBuilder $ container
* @param array $ config
* /
public function load (ContainerBuilder $ container, array $ config)
{
$ definition = new Definition ('DossierBehatSearchSearchController', array (
new Reference (GherkinExtension :: MANAGER_ID)
));
$ definition-> addTag (CliExtension :: CONTROLLER_TAG, array ('priority' => 1));
$ container-> setDefinition (
CliExtension :: CONTROLLER_TAG. '. search',
$ definition
);
}
}

 

 
In the method SearchExntesion :: load Service is thrown. SearchController , which is responsible directly for the declaration of parameters and their reception /processing.
 

 
    use BehatTestworkCliController;
use DossierRegistry;
use DossierUserCriteriaFullnameCriteria;
use SymfonyComponentConsoleCommandCommand as SymfonyCommand;
use SymfonyComponentConsoleExceptioninvalidOptionException;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleInputInputOption;
use SymfonyComponentConsoleOutputOutputInterface;
class SearchController implements Controller
{
const SEARCH_BY_FULLNAME = 'search-by-fullname';
/**
* Configures the command to be executable by the controller.
* @param SymfonyCommand $ command
* /
public function configure (SymfonyCommand $ command)
{
$ command-> addOption ('-'. self :: SEARCH_BY_FULLNAME,
null,
InputOption :: VALUE_OPTIONAL,
"Specify the search query based on the fullname of the user.
Must be started from surname"
.);
}
/**
* Executes controller.
*
* @param InputInterface $ input
* @param OutputInterface $ output
*
* @return null | integer
* /
public function execute (InputInterface $ input, OutputInterface $ output)
{
$ reflect = new ReflectionClass (__ CLASS__);
foreach ($ reflect-> getConstants () as $ constName => $ option) {
if ($ input-> hasOption ($ option) && ($ optValue = $ input-> getOption ($ option))) {
$ queryArgs = explode (',', $ optValue);
Registry :: set ('query', new FullnameCriteria (
) $ QueryArgs[0],
$ QueryArgs[1]?? null,
$ QueryArgs[2]?? null)
);
return null;
}
}
throw new InvalidOptionException ("You must specify one of the following
options to proceed:". implode (',', $ reflect-> getConstants ()));
}
}

 
If everything is declared correctly, the list of available behat commands will be supplemented with the new argument --search-by-fullname :
 

 
    $ vendor /bin /behat --help    

 
   [[email protected] dossier.io]$ vendor /bin /behat --help | grep search-by-fullname
--search-by-fullname[=SEARCH-BY-FULLNAME]Specify the search query based on fullname of the user. Must be started from surname
[[email protected] dossier.io]$

 
Having received the input data inside the SearchController, they can be passed to the Context classes directly, or saved in the database, etc. In the example above, I use the Registry pattern for this. The approach is quite working, but if you know how to do it differently, please tell in the comments.
 
 
That's all. Thank you for attention!
+ 0 -

Add comment