Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
MagentoWebDriver.php
Go to the documentation of this file.
1 <?php
8 
9 use Codeception\Module\WebDriver;
10 use Codeception\Test\Descriptor;
11 use Codeception\TestInterface;
12 use Facebook\WebDriver\Interactions\WebDriverActions;
13 use Codeception\Exception\ModuleConfigException;
14 use Codeception\Exception\ModuleException;
15 use Codeception\Util\Uri;
21 use Yandex\Allure\Adapter\Support\AttachmentSupport;
23 
44 class MagentoWebDriver extends WebDriver
45 {
46  use AttachmentSupport;
47 
52  public static $loadingMasksLocators = [
53  '//div[contains(@class, "loading-mask")]',
54  '//div[contains(@class, "admin_data-grid-loading-mask")]',
55  '//div[contains(@class, "admin__data-grid-loading-mask")]',
56  '//div[contains(@class, "admin__form-loading-mask")]',
57  '//div[@data-role="spinner"]'
58  ];
59 
65  protected $requiredFields = [
66  'url',
67  'backend_name',
68  'username',
69  'password',
70  'browser'
71  ];
72 
78  protected static $localeAll = [
79  LC_COLLATE => null,
80  LC_CTYPE => null,
81  LC_MONETARY => null,
82  LC_NUMERIC => null,
83  LC_TIME => null,
84  LC_MESSAGES => null,
85  ];
86 
92  private $current_test;
93 
99  private $pngReport;
100 
106  private $htmlReport;
107 
113  private $jsErrors = [];
114 
119  public function _initialize()
120  {
121  $this->config = ConfigSanitizerUtil::sanitizeWebDriverConfig($this->config);
122  parent::_initialize();
123  $this->cleanJsError();
124  }
125 
131  public function _resetConfig()
132  {
133  parent::_resetConfig();
134  $this->config = ConfigSanitizerUtil::sanitizeWebDriverConfig($this->config);
135  $this->cleanJsError();
136  }
137 
143  public function _runAfter(TestInterface $test)
144  {
145  parent::_after($test); // TODO: Change the autogenerated stub
146  }
147 
154  public function _after(TestInterface $test)
155  {
156  // DO NOT RESET SESSIONS
157  }
158 
166  public function _getUrl()
167  {
168  if (!isset($this->config['url'])) {
169  throw new ModuleConfigException(
170  __CLASS__,
171  "Module connection failure. The URL for client can't bre retrieved"
172  );
173  }
174  return $this->config['url'];
175  }
176 
184  public function _getCurrentUri()
185  {
186  $url = $this->webDriver->getCurrentURL();
187  if ($url == 'about:blank') {
188  throw new ModuleException($this, 'Current url is blank, no page was opened');
189  }
190  return Uri::retrieveUri($url);
191  }
192 
199  public function dontSeeCurrentUrlEquals($url)
200  {
201  $this->assertNotEquals($url, $this->webDriver->getCurrentURL());
202  }
203 
210  public function dontSeeCurrentUrlMatches($regex)
211  {
212  $this->assertNotRegExp($regex, $this->webDriver->getCurrentURL());
213  }
214 
221  public function dontSeeInCurrentUrl($needle)
222  {
223  $this->assertNotContains($needle, $this->webDriver->getCurrentURL());
224  }
225 
232  public function grabFromCurrentUrl($regex = null)
233  {
234  $fullUrl = $this->webDriver->getCurrentURL();
235  if (!$regex) {
236  return $fullUrl;
237  }
238  $matches = [];
239  $res = preg_match($regex, $fullUrl, $matches);
240  if (!$res) {
241  $this->fail("Couldn't match $regex in " . $fullUrl);
242  }
243  if (!isset($matches[1])) {
244  $this->fail("Nothing to grab. A regex parameter with a capture group is required. Ex: '/(foo)(bar)/'");
245  }
246  return $matches[1];
247  }
248 
255  public function seeCurrentUrlEquals($url)
256  {
257  $this->assertEquals($url, $this->webDriver->getCurrentURL());
258  }
259 
266  public function seeCurrentUrlMatches($regex)
267  {
268  $this->assertRegExp($regex, $this->webDriver->getCurrentURL());
269  }
270 
277  public function seeInCurrentUrl($needle)
278  {
279  $this->assertContains($needle, $this->webDriver->getCurrentURL());
280  }
281 
287  public function closeAdminNotification()
288  {
289  // Cheating here for the minute. Still working on the best method to deal with this issue.
290  try {
291  $this->executeJS("jQuery('.modal-popup').remove(); jQuery('.modals-overlay').remove();");
292  } catch (\Exception $e) {
293  }
294  }
295 
306  public function searchAndMultiSelectOption($select, array $options, $requireAction = false)
307  {
308  $selectDropdown = $select . ' .action-select.admin__action-multiselect';
309  $selectSearchText = $select
310  . ' .admin__action-multiselect-search-wrap>input[data-role="advanced-select-text"]';
311  $selectSearchResult = $select . ' .admin__action-multiselect-label>span';
312 
313  $this->waitForPageLoad();
314  $this->waitForElementVisible($selectDropdown);
315  $this->click($selectDropdown);
316 
317  $this->selectMultipleOptions($selectSearchText, $selectSearchResult, $options);
318 
319  if ($requireAction) {
320  $selectAction = $select . ' button[class=action-default]';
321  $this->waitForPageLoad();
322  $this->click($selectAction);
323  }
324  }
325 
335  public function selectMultipleOptions($selectSearchTextField, $selectSearchResult, array $options)
336  {
337  foreach ($options as $option) {
338  $this->waitForPageLoad();
339  $this->fillField($selectSearchTextField, '');
340  $this->waitForPageLoad();
341  $this->fillField($selectSearchTextField, $option);
342  $this->waitForPageLoad();
343  $this->click($selectSearchResult);
344  }
345  }
346 
353  public function waitForAjaxLoad($timeout = null)
354  {
355  $timeout = $timeout ?? $this->_getConfig()['pageload_timeout'];
356 
357  try {
358  $this->waitForJS('return !!window.jQuery && window.jQuery.active == 0;', $timeout);
359  } catch (\Exception $exceptione) {
360  $this->debug("js never executed, performing {$timeout} second wait.");
361  $this->wait($timeout);
362  }
363  $this->wait(1);
364  }
365 
373  public function waitForPageLoad($timeout = null)
374  {
375  $timeout = $timeout ?? $this->_getConfig()['pageload_timeout'];
376 
377  $this->waitForJS('return document.readyState == "complete"', $timeout);
378  $this->waitForAjaxLoad($timeout);
379  $this->waitForLoadingMaskToDisappear($timeout);
380  }
381 
389  public function waitForLoadingMaskToDisappear($timeout = null)
390  {
391  foreach (self::$loadingMasksLocators as $maskLocator) {
392  // Get count of elements found for looping.
393  // Elements are NOT useful for interaction, as they cannot be fed to codeception actions.
394  $loadingMaskElements = $this->_findElements($maskLocator);
395  for ($i = 1; $i <= count($loadingMaskElements); $i++) {
396  // Formatting and looping on i as we can't interact elements returned above
397  // eg. (//div[@data-role="spinner"])[1]
398  $this->waitForElementNotVisible("({$maskLocator})[{$i}]", $timeout);
399  }
400  }
401  }
402 
408  public function formatMoney(float $money, $locale = 'en_US.UTF-8')
409  {
410  $this->mSetLocale(LC_MONETARY, $locale);
411  $money = money_format('%.2n', $money);
412  $this->mResetLocale();
413  $prefix = substr($money, 0, 1);
414  $number = substr($money, 1);
415  return ['prefix' => $prefix, 'number' => $number];
416  }
417 
424  public function parseFloat($floatString)
425  {
426  $floatString = str_replace(',', '', $floatString);
427  return floatval($floatString);
428  }
429 
435  public function mSetLocale(int $category, $locale)
436  {
437  if (self::$localeAll[$category] == $locale) {
438  return;
439  }
440  foreach (self::$localeAll as $c => $l) {
441  self::$localeAll[$c] = setlocale($c, 0);
442  }
443  setlocale($category, $locale);
444  }
445 
450  public function mResetLocale()
451  {
452  foreach (self::$localeAll as $c => $l) {
453  if ($l !== null) {
454  setlocale($c, $l);
455  self::$localeAll[$c] = null;
456  }
457  }
458  }
459 
464  public function scrollToTopOfPage()
465  {
466  $this->executeJS('window.scrollTo(0,0);');
467  }
468 
476  public function magentoCLI($command, $arguments = null)
477  {
478  // Remove index.php if it's present in url
479  $baseUrl = rtrim(
480  str_replace('index.php', '', rtrim($this->config['url'], '/')),
481  '/'
482  );
483  $apiURL = $baseUrl . '/' . ltrim(getenv('MAGENTO_CLI_COMMAND_PATH'), '/');
484 
485  $executor = new CurlTransport();
486  $executor->write(
487  $apiURL,
488  [
489  getenv('MAGENTO_CLI_COMMAND_PARAMETER') => $command,
490  'arguments' => $arguments
491  ],
493  []
494  );
495  $response = $executor->read();
496  $executor->close();
497  return $response;
498  }
499 
506  public function deleteEntityByUrl($url)
507  {
508  $executor = new WebapiExecutor(null);
509  $executor->write($url, [], CurlInterface::DELETE, []);
510  $response = $executor->read();
511  $executor->close();
512  return $response;
513  }
514 
524  public function conditionalClick($selector, $dependentSelector, $visible)
525  {
526  $el = $this->_findElements($dependentSelector);
527  if (sizeof($el) > 1) {
528  throw new \Exception("more than one element matches selector " . $dependentSelector);
529  }
530 
531  $clickCondition = null;
532  if ($visible) {
533  $clickCondition = !empty($el) && $el[0]->isDisplayed();
534  } else {
535  $clickCondition = empty($el) || !$el[0]->isDisplayed();
536  }
537 
538  if ($clickCondition) {
539  $this->click($selector);
540  }
541  }
542 
549  public function clearField($selector)
550  {
551  $this->fillField($selector, "");
552  }
553 
562  public function assertElementContainsAttribute($selector, $attribute, $value)
563  {
564  $attributes = $this->grabAttributeFrom($selector, $attribute);
565 
566  if (isset($value) && empty($value)) {
567  // If an "attribute" is blank, "", or null we need to be able to assert that it's present.
568  // When an "attribute" is blank or null it returns "true" so we assert that "true" is present.
569  $this->assertEquals($attributes, 'true');
570  } else {
571  $this->assertContains($value, $attributes);
572  }
573  }
574 
580  public function _before(TestInterface $test)
581  {
582  $this->current_test = $test;
583  $this->htmlReport = null;
584  $this->pngReport = null;
585 
586  parent::_before($test);
587  }
588 
597  public function dragAndDrop($source, $target, $xOffset = null, $yOffset = null)
598  {
599  if ($xOffset !== null || $yOffset !== null) {
600  $snodes = $this->matchFirstOrFail($this->baseElement, $source);
601  $tnodes = $this->matchFirstOrFail($this->baseElement, $target);
602 
603  $targetX = intval($tnodes->getLocation()->getX() + $xOffset);
604  $targetY = intval($tnodes->getLocation()->getY() + $yOffset);
605 
606  $travelX = intval($targetX - $snodes->getLocation()->getX());
607  $travelY = intval($targetY - $snodes->getLocation()->getY());
608 
609  $action = new WebDriverActions($this->webDriver);
610  $action->moveToElement($snodes)->perform();
611  $action->clickAndHold($snodes)->perform();
612  $action->moveByOffset($travelX, $travelY)->perform();
613  $action->release()->perform();
614  } else {
615  parent::dragAndDrop($source, $target);
616  }
617  }
618 
627  public function fillSecretField($field, $value)
628  {
629  // to protect any secrets from being printed to console the values are executed only at the webdriver level as a
630  // decrypted value
631 
632  $decryptedValue = CredentialStore::getInstance()->decryptSecretValue($value);
633  $this->fillField($field, $decryptedValue);
634  }
635 
644  public function _failed(TestInterface $test, $fail)
645  {
646  $this->debugWebDriverLogs($test);
647 
648  if ($this->pngReport === null && $this->htmlReport === null) {
649  $this->saveScreenshot();
650  }
651 
652  if ($this->current_test == null) {
653  throw new \RuntimeException("Suite condition failure: \n" . $fail->getMessage());
654  }
655 
656  $this->addAttachment($this->pngReport, $test->getMetadata()->getName() . '.png', 'image/png');
657  $this->addAttachment($this->htmlReport, $test->getMetadata()->getName() . '.html', 'text/html');
658 
659  $this->debug("Failure due to : {$fail->getMessage()}");
660  $this->debug("Screenshot saved to {$this->pngReport}");
661  $this->debug("Html saved to {$this->htmlReport}");
662  }
663 
668  public function saveScreenshot()
669  {
670  $testDescription = "unknown." . uniqid();
671  if ($this->current_test != null) {
672  $testDescription = Descriptor::getTestSignature($this->current_test);
673  }
674 
675  $filename = preg_replace('~\W~', '.', $testDescription);
676  $outputDir = codecept_output_dir();
677  $this->_saveScreenshot($this->pngReport = $outputDir . mb_strcut($filename, 0, 245, 'utf-8') . '.fail.png');
678  $this->_savePageSource($this->htmlReport = $outputDir . mb_strcut($filename, 0, 244, 'utf-8') . '.fail.html');
679  }
680 
688  public function amOnPage($page)
689  {
690  parent::amOnPage($page);
691  $this->waitForPageLoad();
692  }
693 
701  public function skipReadinessCheck($check)
702  {
703  $this->config['skipReadiness'] = $check;
704  }
705 
711  public function cleanJsError()
712  {
713  $this->jsErrors = [];
714  }
715 
722  public function setJsError($errMsg)
723  {
724  $this->jsErrors[] = $errMsg;
725  }
726 
732  private function getJsErrors()
733  {
734  $errors = '';
735 
736  if (!empty($this->jsErrors)) {
737  $errors = 'Errors in JavaScript:';
738  foreach ($this->jsErrors as $jsError) {
739  $errors .= "\n" . $jsError;
740  }
741  }
742  return $errors;
743  }
744 
750  public function dontSeeJsError()
751  {
752  $this->assertEmpty($this->jsErrors, $this->getJsErrors());
753  }
754 }
$response
Definition: 404.php:11
static sanitizeWebDriverConfig($config, $params=['url', 'selenium'])
conditionalClick($selector, $dependentSelector, $visible)
$number
Definition: details.phtml:22
searchAndMultiSelectOption($select, array $options, $requireAction=false)
$source
Definition: source.php:23
selectMultipleOptions($selectSearchTextField, $selectSearchResult, array $options)
$target
Definition: skip.phtml:8
$prefix
Definition: name.phtml:25
$value
Definition: gender.phtml:16
$page
Definition: pages.php:8
dragAndDrop($source, $target, $xOffset=null, $yOffset=null)
$attributes
Definition: matrix.phtml:13
$arguments
if(isset($opts->o)) if(! $usingStdout) $l
$i
Definition: gallery.phtml:31
$errors
Definition: overview.phtml:9