forked from EVOgeek/Vmeda.Online
		
	This change allows you to write and run Behat tests that cover the mobile app. These should have the @app tag. They will be run in the Chrome browser using an Ionic server on the local machine. See config-dist.php for configuration settings, or full docs here: https://docs.moodle.org/dev/Acceptance_testing_for_the_mobile_app
		
			
				
	
	
		
			525 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			525 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| // This file is part of Moodle - http://moodle.org/
 | |
| //
 | |
| // Moodle 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 3 of the License, or
 | |
| // (at your option) any later version.
 | |
| //
 | |
| // Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | |
| 
 | |
| /**
 | |
|  * Mobile/desktop app steps definitions.
 | |
|  *
 | |
|  * @package core
 | |
|  * @category test
 | |
|  * @copyright 2018 The Open University
 | |
|  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | |
|  */
 | |
| 
 | |
| // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 | |
| 
 | |
| require_once(__DIR__ . '/../../behat/behat_base.php');
 | |
| 
 | |
| use Behat\Mink\Exception\DriverException;
 | |
| use Behat\Mink\Exception\ExpectationException;
 | |
| use Behat\Behat\Hook\Scope\BeforeScenarioScope;
 | |
| 
 | |
| /**
 | |
|  * Mobile/desktop app steps definitions.
 | |
|  *
 | |
|  * @package core
 | |
|  * @category test
 | |
|  * @copyright 2018 The Open University
 | |
|  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | |
|  */
 | |
| class behat_app extends behat_base {
 | |
|     /** @var bool True if the current scenario has the app tag */
 | |
|     protected $apptag = false;
 | |
| 
 | |
|     /** @var stdClass Object with data about launched Ionic instance (if any) */
 | |
|     protected static $ionicrunning = null;
 | |
| 
 | |
|     /**
 | |
|      * Checks if the current OS is Windows, from the point of view of task-executing-and-killing.
 | |
|      *
 | |
|      * @return bool True if Windows
 | |
|      */
 | |
|     protected static function is_windows() : bool {
 | |
|         return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Checks tags before each scenario.
 | |
|      *
 | |
|      * @BeforeScenario
 | |
|      * @param BeforeScenarioScope $scope Scope information
 | |
|      */
 | |
|     public function check_tags(BeforeScenarioScope $scope) {
 | |
|         $this->apptag = in_array('app', $scope->getScenario()->getTags()) ||
 | |
|                 in_array('app', $scope->getFeature()->getTags());
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Opens the Moodle app in the browser.
 | |
|      *
 | |
|      * Requires JavaScript.
 | |
|      *
 | |
|      * @Given /^I enter the app$/
 | |
|      * @throws DriverException Issue with configuration or feature file
 | |
|      * @throws dml_exception Problem with Moodle setup
 | |
|      * @throws ExpectationException Problem with resizing window
 | |
|      */
 | |
|     public function i_enter_the_app() {
 | |
|         // Restart the browser and set its size.
 | |
|         $this->getSession()->restart();
 | |
|         $this->resize_window('360x720', true);
 | |
| 
 | |
|         // Prepare setup.
 | |
|         $this->check_behat_setup();
 | |
|         $this->fix_moodle_setup();
 | |
| 
 | |
|         // Start Ionic server (or use existing one).
 | |
|         $url = $this->start_or_reuse_ionic();
 | |
| 
 | |
|         // Go to page and prepare browser for app.
 | |
|         $this->prepare_browser($url);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Checks the Behat setup - tags and configuration.
 | |
|      *
 | |
|      * @throws DriverException
 | |
|      */
 | |
|     protected function check_behat_setup() {
 | |
|         global $CFG;
 | |
| 
 | |
|         // Check the app tag was set.
 | |
|         if (!$this->apptag) {
 | |
|             throw new DriverException('Requires @app tag on scenario or feature.');
 | |
|         }
 | |
| 
 | |
|         // Check JavaScript is enabled.
 | |
|         if (!$this->running_javascript()) {
 | |
|             throw new DriverException('The app requires JavaScript.');
 | |
|         }
 | |
| 
 | |
|         // Check the config settings are defined.
 | |
|         if (empty($CFG->behat_ionicaddress) && empty($CFG->behat_approot)) {
 | |
|             throw new DriverException('$CFG->behat_ionicaddress or $CFG->behat_approot must be defined.');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Fixes the Moodle admin settings to allow mobile app use (if not already correct).
 | |
|      *
 | |
|      * @throws dml_exception If there is any problem changing Moodle settings
 | |
|      */
 | |
|     protected function fix_moodle_setup() {
 | |
|         global $CFG, $DB;
 | |
| 
 | |
|         // Configure Moodle settings to enable app web services.
 | |
|         if (!$CFG->enablewebservices) {
 | |
|             set_config('enablewebservices', 1);
 | |
|         }
 | |
|         if (!$CFG->enablemobilewebservice) {
 | |
|             set_config('enablemobilewebservice', 1);
 | |
|         }
 | |
| 
 | |
|         // Add 'Create token' and 'Use REST webservice' permissions to authenticated user role.
 | |
|         $userroleid = $DB->get_field('role', 'id', ['shortname' => 'user']);
 | |
|         $systemcontext = \context_system::instance();
 | |
|         role_change_permission($userroleid, $systemcontext, 'moodle/webservice:createtoken', CAP_ALLOW);
 | |
|         role_change_permission($userroleid, $systemcontext, 'webservice/rest:use', CAP_ALLOW);
 | |
| 
 | |
|         // Check the value of the 'webserviceprotocols' config option. Due to weird behaviour
 | |
|         // in Behat with regard to config variables that aren't defined in a settings.php, the
 | |
|         // value in $CFG here may reflect a previous run, so get it direct from the database
 | |
|         // instead.
 | |
|         $field = $DB->get_field('config', 'value', ['name' => 'webserviceprotocols'], IGNORE_MISSING);
 | |
|         if (empty($field)) {
 | |
|             $protocols = [];
 | |
|         } else {
 | |
|             $protocols = explode(',', $field);
 | |
|         }
 | |
|         if (!in_array('rest', $protocols)) {
 | |
|             $protocols[] = 'rest';
 | |
|             set_config('webserviceprotocols', implode(',', $protocols));
 | |
|         }
 | |
| 
 | |
|         // Enable mobile service.
 | |
|         require_once($CFG->dirroot . '/webservice/lib.php');
 | |
|         $webservicemanager = new webservice();
 | |
|         $service = $webservicemanager->get_external_service_by_shortname(
 | |
|                 MOODLE_OFFICIAL_MOBILE_SERVICE, MUST_EXIST);
 | |
|         if (!$service->enabled) {
 | |
|             $service->enabled = 1;
 | |
|             $webservicemanager->update_external_service($service);
 | |
|         }
 | |
| 
 | |
|         // If installed, also configure local_mobile plugin to enable additional features service.
 | |
|         $localplugins = core_component::get_plugin_list('local');
 | |
|         if (array_key_exists('mobile', $localplugins)) {
 | |
|             $service = $webservicemanager->get_external_service_by_shortname(
 | |
|                     'local_mobile', MUST_EXIST);
 | |
|             if (!$service->enabled) {
 | |
|                 $service->enabled = 1;
 | |
|                 $webservicemanager->update_external_service($service);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Starts an Ionic server if necessary, or uses an existing one.
 | |
|      *
 | |
|      * @return string URL to Ionic server
 | |
|      * @throws DriverException If there's a system error starting Ionic
 | |
|      */
 | |
|     protected function start_or_reuse_ionic() {
 | |
|         global $CFG;
 | |
| 
 | |
|         if (!empty($CFG->behat_ionicaddress)) {
 | |
|             // Use supplied Ionic server which should already be running.
 | |
|             $url = $CFG->behat_ionicaddress;
 | |
|         } else if (self::$ionicrunning) {
 | |
|             // Use existing Ionic instance launched previously.
 | |
|             $url = self::$ionicrunning->url;
 | |
|         } else {
 | |
|             // Open Ionic process in relevant path.
 | |
|             $path = realpath($CFG->behat_approot);
 | |
|             $stderrfile = $CFG->dataroot . '/behat/ionic-stderr.log';
 | |
|             $prefix = '';
 | |
|             // Except on Windows, use 'exec' so that we get the pid of the actual Node process
 | |
|             // and not the shell it uses to execute. You can't do exec on Windows; there is a
 | |
|             // bypass_shell option but it is not the same thing and isn't usable here.
 | |
|             if (!self::is_windows()) {
 | |
|                 $prefix = 'exec ';
 | |
|             }
 | |
|             $process = proc_open($prefix . 'ionic serve --no-interactive --no-open',
 | |
|                     [['pipe', 'r'], ['pipe', 'w'], ['file', $stderrfile, 'w']], $pipes, $path);
 | |
|             if ($process === false) {
 | |
|                 throw new DriverException('Error starting Ionic process');
 | |
|             }
 | |
|             fclose($pipes[0]);
 | |
| 
 | |
|             // Get pid - we will need this to kill the process.
 | |
|             $status = proc_get_status($process);
 | |
|             $pid = $status['pid'];
 | |
| 
 | |
|             // Read data from stdout until the server comes online.
 | |
|             // Note: On Windows it is impossible to read simultaneously from stderr and stdout
 | |
|             // because stream_select and non-blocking I/O don't work on process pipes, so that is
 | |
|             // why stderr was redirected to a file instead. Also, this code is simpler.
 | |
|             $url = null;
 | |
|             $stdoutlog = '';
 | |
|             while (true) {
 | |
|                 $line = fgets($pipes[1], 4096);
 | |
|                 if ($line === false) {
 | |
|                     break;
 | |
|                 }
 | |
| 
 | |
|                 $stdoutlog .= $line;
 | |
| 
 | |
|                 if (preg_match('~^\s*Local: (http\S*)~', $line, $matches)) {
 | |
|                     $url = $matches[1];
 | |
|                     break;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             // If it failed, close the pipes and the process.
 | |
|             if (!$url) {
 | |
|                 fclose($pipes[1]);
 | |
|                 proc_close($process);
 | |
|                 $logpath = $CFG->dataroot . '/behat/ionic-start.log';
 | |
|                 $stderrlog = file_get_contents($stderrfile);
 | |
|                 @unlink($stderrfile);
 | |
|                 file_put_contents($logpath,
 | |
|                         "Ionic startup log from " . date('c') .
 | |
|                         "\n\n----STDOUT----\n$stdoutlog\n\n----STDERR----\n$stderrlog");
 | |
|                 throw new DriverException('Unable to start Ionic. See ' . $logpath);
 | |
|             }
 | |
| 
 | |
|             // Remember the URL, so we can reuse it next time, and other details so we can kill
 | |
|             // the process.
 | |
|             self::$ionicrunning = (object)['url' => $url, 'process' => $process, 'pipes' => $pipes,
 | |
|                     'pid' => $pid];
 | |
|         }
 | |
|         return $url;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Closes Ionic (if it was started) at end of test suite.
 | |
|      *
 | |
|      * @AfterSuite
 | |
|      */
 | |
|     public static function close_ionic() {
 | |
|         if (self::$ionicrunning) {
 | |
|             fclose(self::$ionicrunning->pipes[1]);
 | |
| 
 | |
|             if (self::is_windows()) {
 | |
|                 // Using proc_terminate here does not work. It terminates the process but not any
 | |
|                 // other processes it might have launched. Instead, we need to use an OS-specific
 | |
|                 // mechanism to kill the process and children based on its pid.
 | |
|                 exec('taskkill /F /T /PID ' . self::$ionicrunning->pid);
 | |
|             } else {
 | |
|                 // On Unix this actually works, although only due to the 'exec' command inserted
 | |
|                 // above.
 | |
|                 proc_terminate(self::$ionicrunning->process);
 | |
|             }
 | |
|             self::$ionicrunning = null;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Goes to the app page and then sets up some initial JavaScript so we can use it.
 | |
|      *
 | |
|      * @param string $url App URL
 | |
|      * @throws DriverException If the app fails to load properly
 | |
|      */
 | |
|     protected function prepare_browser(string $url) {
 | |
|         global $CFG;
 | |
| 
 | |
|         // Visit the Ionic URL and wait for it to load.
 | |
|         $this->getSession()->visit($url);
 | |
|         $this->spin(
 | |
|                 function($context, $args) {
 | |
|                     $title = $context->getSession()->getPage()->find('xpath', '//title');
 | |
|                     if ($title) {
 | |
|                         $text = $title->getHtml();
 | |
|                         if ($text === 'Moodle Desktop') {
 | |
|                             return true;
 | |
|                         }
 | |
|                     }
 | |
|                     throw new DriverException('Moodle app not found in browser');
 | |
|                 }, false, 30);
 | |
| 
 | |
|         // Run the scripts to install Moodle 'pending' checks.
 | |
|         $this->getSession()->executeScript(
 | |
|                 file_get_contents(__DIR__ . '/app_behat_runtime.js'));
 | |
| 
 | |
|         // Wait until the site login field appears OR the main page.
 | |
|         $situation = $this->spin(
 | |
|                 function($context, $args) {
 | |
|                     $input = $context->getSession()->getPage()->find('xpath', '//input[@name="url"]');
 | |
|                     if ($input) {
 | |
|                         return 'login';
 | |
|                     }
 | |
|                     $mainmenu = $context->getSession()->getPage()->find('xpath', '//page-core-mainmenu');
 | |
|                     if ($mainmenu) {
 | |
|                         return 'mainpage';
 | |
|                     }
 | |
|                     throw new DriverException('Moodle app login URL prompt not found');
 | |
|                 }, false, 30);
 | |
| 
 | |
|         // If it's the login page, we automatically fill in the URL and leave it on the user/pass
 | |
|         // page. If it's the main page, we just leave it there.
 | |
|         if ($situation === 'login') {
 | |
|             $this->i_set_the_field_in_the_app('Site address', $CFG->wwwroot);
 | |
|             $this->i_press_in_the_app('Connect!');
 | |
|         }
 | |
| 
 | |
|         // Continue only after JS finishes.
 | |
|         $this->wait_for_pending_js();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Logs in as the given user in the app's login screen.
 | |
|      *
 | |
|      * Must be run from the app login screen (i.e. immediately after first 'I enter the app').
 | |
|      *
 | |
|      * @Given /^I log in as "(?P<username_string>(?:[^"]|\\")*)" in the app$/
 | |
|      * @param string $username Username (and password)
 | |
|      * @throws DriverException If the main page doesn't load
 | |
|      */
 | |
|     public function i_log_in_as_username_in_the_app(string $username) {
 | |
|         $this->i_set_the_field_in_the_app('Username', $username);
 | |
|         $this->i_set_the_field_in_the_app('Password', $username);
 | |
| 
 | |
|         // Note there are two 'Log in' texts visible (the title and the button) so we have to use
 | |
|         // the 'near' syntax here.
 | |
|         $this->i_press_near_in_the_app('Log in', 'Forgotten');
 | |
| 
 | |
|         // Wait until the main page appears.
 | |
|         $this->spin(
 | |
|                 function($context, $args) {
 | |
|                     $mainmenu = $context->getSession()->getPage()->find('xpath', '//page-core-mainmenu');
 | |
|                     if ($mainmenu) {
 | |
|                         return 'mainpage';
 | |
|                     }
 | |
|                     throw new DriverException('Moodle app main page not loaded after login');
 | |
|                 }, false, 30);
 | |
| 
 | |
|         // Wait for JS to finish as well.
 | |
|         $this->wait_for_pending_js();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Presses standard buttons in the app.
 | |
|      *
 | |
|      * @Given /^I press the (?P<button_name>back|main menu|page menu) button in the app$/
 | |
|      * @param string $button Button type
 | |
|      * @throws DriverException If the button push doesn't work
 | |
|      */
 | |
|     public function i_press_the_standard_button_in_the_app(string $button) {
 | |
|         $this->spin(function($context, $args) use ($button) {
 | |
|             $result = $this->getSession()->evaluateScript('return window.behatPressStandard("' .
 | |
|                     $button . '");');
 | |
|             if ($result !== 'OK') {
 | |
|                 throw new DriverException('Error pressing standard button - ' . $result);
 | |
|             }
 | |
|             return true;
 | |
|         });
 | |
|         $this->wait_for_pending_js();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Closes a popup by clicking on the 'backdrop' behind it.
 | |
|      *
 | |
|      * @Given /^I close the popup in the app$/
 | |
|      * @throws DriverException If there isn't a popup to close
 | |
|      */
 | |
|     public function i_close_the_popup_in_the_app() {
 | |
|         $this->spin(function($context, $args)  {
 | |
|             $result = $this->getSession()->evaluateScript('return window.behatClosePopup();');
 | |
|             if ($result !== 'OK') {
 | |
|                 throw new DriverException('Error closing popup - ' . $result);
 | |
|             }
 | |
|             return true;
 | |
|         });
 | |
|         $this->wait_for_pending_js();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Clicks on / touches something that is visible in the app.
 | |
|      *
 | |
|      * Note it is difficult to use the standard 'click on' or 'press' steps because those do not
 | |
|      * distinguish visible items and the app always has many non-visible items in the DOM.
 | |
|      *
 | |
|      * @Given /^I press "(?P<text_string>(?:[^"]|\\")*)" in the app$/
 | |
|      * @param string $text Text identifying click target
 | |
|      * @throws DriverException If the press doesn't work
 | |
|      */
 | |
|     public function i_press_in_the_app(string $text) {
 | |
|         $this->spin(function($context, $args) use ($text) {
 | |
|             $result = $this->getSession()->evaluateScript('return window.behatPress("' .
 | |
|                     addslashes_js($text) . '");');
 | |
|             if ($result !== 'OK') {
 | |
|                 throw new DriverException('Error pressing item - ' . $result);
 | |
|             }
 | |
|             return true;
 | |
|         });
 | |
|         $this->wait_for_pending_js();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Clicks on / touches something that is visible in the app, near some other text.
 | |
|      *
 | |
|      * This is the same as the other step, but when there are multiple matches, it picks the one
 | |
|      * nearest (in DOM terms) the second text. The second text should be an exact match, or a partial
 | |
|      * match that only has one result.
 | |
|      *
 | |
|      * @Given /^I press "(?P<text_string>(?:[^"]|\\")*)" near "(?P<nearby_string>(?:[^"]|\\")*)" in the app$/
 | |
|      * @param string $text Text identifying click target
 | |
|      * @param string $near Text identifying a nearby unique piece of text
 | |
|      * @throws DriverException If the press doesn't work
 | |
|      */
 | |
|     public function i_press_near_in_the_app(string $text, string $near) {
 | |
|         $this->spin(function($context, $args) use ($text, $near) {
 | |
|             $result = $this->getSession()->evaluateScript('return window.behatPress("' .
 | |
|                     addslashes_js($text) . '", "' . addslashes_js($near) . '");');
 | |
|             if ($result !== 'OK') {
 | |
|                 throw new DriverException('Error pressing item - ' . $result);
 | |
|             }
 | |
|             return true;
 | |
|         });
 | |
|         $this->wait_for_pending_js();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Sets a field to the given text value in the app.
 | |
|      *
 | |
|      * Currently this only works for input fields which must be identified using a partial or
 | |
|      * exact match on the placeholder text.
 | |
|      *
 | |
|      * @Given /^I set the field "(?P<field_name>(?:[^"]|\\")*)" to "(?P<text_string>(?:[^"]|\\")*)" in the app$/
 | |
|      * @param string $field Text identifying field
 | |
|      * @param string $value Value for field
 | |
|      * @throws DriverException If the field set doesn't work
 | |
|      */
 | |
|     public function i_set_the_field_in_the_app(string $field, string $value) {
 | |
|         $this->spin(function($context, $args) use ($field, $value) {
 | |
|             $result = $this->getSession()->evaluateScript('return window.behatSetField("' .
 | |
|                     addslashes_js($field) . '", "' . addslashes_js($value) . '");');
 | |
|             if ($result !== 'OK') {
 | |
|                 throw new DriverException('Error setting field - ' . $result);
 | |
|             }
 | |
|             return true;
 | |
|         });
 | |
|         $this->wait_for_pending_js();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Checks that the current header stripe in the app contains the expected text.
 | |
|      *
 | |
|      * This can be used to see if the app went to the expected page.
 | |
|      *
 | |
|      * @Then /^the header should be "(?P<text_string>(?:[^"]|\\")*)" in the app$/
 | |
|      * @param string $text Expected header text
 | |
|      * @throws DriverException If the header can't be retrieved
 | |
|      * @throws ExpectationException If the header text is different to the expected value
 | |
|      */
 | |
|     public function the_header_should_be_in_the_app(string $text) {
 | |
|         $result = $this->spin(function($context, $args) {
 | |
|             $result = $this->getSession()->evaluateScript('return window.behatGetHeader();');
 | |
|             if (substr($result, 0, 3) !== 'OK:') {
 | |
|                 throw new DriverException('Error getting header - ' . $result);
 | |
|             }
 | |
|             return $result;
 | |
|         });
 | |
|         $header = substr($result, 3);
 | |
|         if (trim($header) !== trim($text)) {
 | |
|             throw new ExpectationException('The header text was not as expected: \'' . $header . '\'',
 | |
|                     $this->getSession()->getDriver());
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Switches to a newly-opened browser tab.
 | |
|      *
 | |
|      * This assumes the app opened a new tab.
 | |
|      *
 | |
|      * @Given /^I switch to the browser tab opened by the app$/
 | |
|      * @throws DriverException If there aren't exactly 2 tabs open
 | |
|      */
 | |
|     public function i_switch_to_the_browser_tab_opened_by_the_app() {
 | |
|         $names = $this->getSession()->getWindowNames();
 | |
|         if (count($names) !== 2) {
 | |
|             throw new DriverException('Expected to see 2 tabs open, not ' . count($names));
 | |
|         }
 | |
|         $this->getSession()->switchToWindow($names[1]);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Closes the current browser tab.
 | |
|      *
 | |
|      * This assumes it was opened by the app and you will now get back to the app.
 | |
|      *
 | |
|      * @Given /^I close the browser tab opened by the app$/
 | |
|      * @throws DriverException If there aren't exactly 2 tabs open
 | |
|      */
 | |
|     public function i_close_the_browser_tab_opened_by_the_app() {
 | |
|         $names = $this->getSession()->getWindowNames();
 | |
|         if (count($names) !== 2) {
 | |
|             throw new DriverException('Expected to see 2 tabs open, not ' . count($names));
 | |
|         }
 | |
|         $this->getSession()->getDriver()->executeScript('window.close()');
 | |
|         $this->getSession()->switchToWindow($names[0]);
 | |
|     }
 | |
| }
 |