WebDriverTestBase.php 6.99 KB
Newer Older
1
2
3
4
5
6
<?php

namespace Drupal\FunctionalJavascriptTests;

use Behat\Mink\Exception\DriverException;
use Drupal\Tests\BrowserTestBase;
7
use PHPUnit\Runner\BaseTestRunner;
8
use Symfony\Component\DependencyInjection\ContainerInterface;
9
10

/**
11
 * Runs a browser test using a driver that supports JavaScript.
12
13
 *
 * Base class for testing browser interaction implemented in JavaScript.
14
15
 *
 * @ingroup testing
16
17
18
 */
abstract class WebDriverTestBase extends BrowserTestBase {

19
20
21
22
23
24
25
26
27
28
  /**
   * Disables CSS animations in tests for more reliable testing.
   *
   * CSS animations are disabled by installing the css_disable_transitions_test
   * module. Set to FALSE to test CSS animations.
   *
   * @var bool
   */
  protected $disableCssAnimations = TRUE;

29
30
31
32
33
  /**
   * {@inheritdoc}
   */
  protected $minkDefaultDriverClass = DrupalSelenium2Driver::class;

34
35
36
37
38
39
40
41
42
  /**
   * The maximum number of times to try a webdriver request.
   *
   * @var int
   *
   * @see \Drupal\FunctionalJavascriptTests\WebDriverCurlService::$maxRetries
   */
  protected const WEBDRIVER_RETRIES = 10;

43
44
45
46
  /**
   * {@inheritdoc}
   */
  protected function initMink() {
47
48
    if (!is_a($this->minkDefaultDriverClass, DrupalSelenium2Driver::class, TRUE)) {
      throw new \UnexpectedValueException(sprintf("%s has to be an instance of %s", $this->minkDefaultDriverClass, DrupalSelenium2Driver::class));
49
    }
50
    $this->minkDefaultDriverArgs = ['chrome', NULL, 'http://localhost:4444'];
51
    WebDriverCurlService::setMaxRetries(max((int) getenv('WEBDRIVER_RETRIES'), static::WEBDRIVER_RETRIES));
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

    try {
      return parent::initMink();
    }
    catch (DriverException $e) {
      if ($this->minkDefaultDriverClass === DrupalSelenium2Driver::class) {
        $this->markTestSkipped("The test wasn't able to connect to your webdriver instance. For more information read core/tests/README.md.\n\nThe original message while starting Mink: {$e->getMessage()}");
      }
      else {
        throw $e;
      }
    }
    catch (\Exception $e) {
      $this->markTestSkipped('An unexpected error occurred while starting Mink: ' . $e->getMessage());
    }
  }

69
70
71
72
  /**
   * {@inheritdoc}
   */
  protected function installModulesFromClassProperty(ContainerInterface $container) {
73
    self::$modules = ['js_deprecation_log_test'];
74
    if ($this->disableCssAnimations) {
75
      self::$modules[] = 'css_disable_transitions_test';
76
77
78
79
    }
    parent::installModulesFromClassProperty($container);
  }

80
81
82
83
84
85
86
87
88
89
  /**
   * {@inheritdoc}
   */
  protected function initFrontPage() {
    parent::initFrontPage();
    // Set a standard window size so that all javascript tests start with the
    // same viewport.
    $this->getSession()->resizeWindow(1024, 768);
  }

90
91
92
93
94
  /**
   * {@inheritdoc}
   */
  protected function tearDown() {
    if ($this->mink) {
95
96
97
98
99
      $status = $this->getStatus();
      if ($status === BaseTestRunner::STATUS_ERROR || $status === BaseTestRunner::STATUS_WARNING || $status === BaseTestRunner::STATUS_FAILURE) {
        // Ensure we capture the output at point of failure.
        @$this->htmlOutput();
      }
100
101
102
103
104
105
106
107
108
109
110
      // Wait for all requests to finish. It is possible that an AJAX request is
      // still on-going.
      $result = $this->getSession()->wait(5000, '(typeof(jQuery)=="undefined" || (0 === jQuery.active && 0 === jQuery(\':animated\').length))');
      if (!$result) {
        // If the wait is unsuccessful, there may still be an AJAX request in
        // progress. If we tear down now, then this AJAX request may fail with
        // missing database tables, because tear down will have removed them.
        // Rather than allow it to fail, throw an explicit exception now
        // explaining what the problem is.
        throw new \RuntimeException('Unfinished AJAX requests while tearing down a test');
      }
111
112
113
114
115
116
117

      $warnings = $this->getSession()->evaluateScript("JSON.parse(sessionStorage.getItem('js_deprecation_log_test.warnings') || JSON.stringify([]))");
      foreach ($warnings as $warning) {
        if (strpos($warning, '[Deprecation]') === 0) {
          @trigger_error('Javascript Deprecation:' . substr($warning, 13), E_USER_DEPRECATED);
        }
      }
118
119
120
121
122
    }
    parent::tearDown();
  }

  /**
123
124
   * {@inheritdoc}
   */
125
126
  protected function getMinkDriverArgs() {
    if ($this->minkDefaultDriverClass === DrupalSelenium2Driver::class) {
127
      return getenv('MINK_DRIVER_ARGS_WEBDRIVER') ?: parent::getMinkDriverArgs();
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
    }
    return parent::getMinkDriverArgs();
  }

  /**
   * Waits for the given time or until the given JS condition becomes TRUE.
   *
   * @param string $condition
   *   JS condition to wait until it becomes TRUE.
   * @param int $timeout
   *   (Optional) Timeout in milliseconds, defaults to 10000.
   * @param string $message
   *   (optional) A message to display with the assertion. If left blank, a
   *   default message will be displayed.
   *
143
   * @throws \PHPUnit\Framework\AssertionFailedError
144
145
146
147
   *
   * @see \Behat\Mink\Driver\DriverInterface::evaluateScript()
   */
  protected function assertJsCondition($condition, $timeout = 10000, $message = '') {
148
    $message = $message ?: "JavaScript condition met:\n" . $condition;
149
150
151
152
153
154
155
156
    $result = $this->getSession()->getDriver()->wait($timeout, $condition);
    $this->assertTrue($result, $message);
  }

  /**
   * Creates a screenshot.
   *
   * @param string $filename
157
   *   The file name of the resulting screenshot including a writable path. For
158
   *   example, /tmp/test_screenshot.jpg.
159
160
   * @param bool $set_background_color
   *   (optional) By default this method will set the background color to white.
161
   *   Set to FALSE to override this behavior.
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
   *
   * @throws \Behat\Mink\Exception\UnsupportedDriverActionException
   *   When operation not supported by the driver.
   * @throws \Behat\Mink\Exception\DriverException
   *   When the operation cannot be done.
   */
  protected function createScreenshot($filename, $set_background_color = TRUE) {
    $session = $this->getSession();
    if ($set_background_color) {
      $session->executeScript("document.body.style.backgroundColor = 'white';");
    }
    $image = $session->getScreenshot();
    file_put_contents($filename, $image);
  }

  /**
   * {@inheritdoc}
   */
  public function assertSession($name = NULL) {
    return new WebDriverWebAssert($this->getSession($name), $this->baseUrl);
  }

  /**
   * Gets the current Drupal javascript settings and parses into an array.
   *
   * Unlike BrowserTestBase::getDrupalSettings(), this implementation reads the
   * current values of drupalSettings, capturing all changes made via javascript
   * after the page was loaded.
   *
   * @return array
   *   The Drupal javascript settings array.
   *
   * @see \Drupal\Tests\BrowserTestBase::getDrupalSettings()
   */
  protected function getDrupalSettings() {
    $script = <<<EndOfScript
(function () {
  if (typeof drupalSettings !== 'undefined') {
    return drupalSettings;
  }
})();
EndOfScript;

    return $this->getSession()->evaluateScript($script) ?: [];
  }

  /**
   * {@inheritdoc}
   */
  protected function getHtmlOutputHeaders() {
    // The webdriver API does not support fetching headers.
    return '';
  }

}