annotate core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\FunctionalJavascriptTests;
Chris@0 4
Chris@0 5 use Behat\Mink\Element\NodeElement;
Chris@0 6 use Behat\Mink\Exception\ElementHtmlException;
Chris@0 7 use Behat\Mink\Exception\ElementNotFoundException;
Chris@0 8 use Behat\Mink\Exception\UnsupportedDriverActionException;
Chris@0 9 use Drupal\Tests\WebAssert;
Chris@0 10
Chris@0 11 /**
Chris@0 12 * Defines a class with methods for asserting presence of elements during tests.
Chris@0 13 */
Chris@0 14 class JSWebAssert extends WebAssert {
Chris@0 15
Chris@0 16 /**
Chris@0 17 * Waits for AJAX request to be completed.
Chris@0 18 *
Chris@0 19 * @param int $timeout
Chris@0 20 * (Optional) Timeout in milliseconds, defaults to 10000.
Chris@0 21 * @param string $message
Chris@0 22 * (optional) A message for exception.
Chris@0 23 *
Chris@0 24 * @throws \RuntimeException
Chris@0 25 * When the request is not completed. If left blank, a default message will
Chris@0 26 * be displayed.
Chris@0 27 */
Chris@0 28 public function assertWaitOnAjaxRequest($timeout = 10000, $message = 'Unable to complete AJAX request.') {
Chris@0 29 $condition = <<<JS
Chris@0 30 (function() {
Chris@0 31 function isAjaxing(instance) {
Chris@0 32 return instance && instance.ajaxing === true;
Chris@0 33 }
Chris@0 34 return (
Chris@0 35 // Assert no AJAX request is running (via jQuery or Drupal) and no
Chris@0 36 // animation is running.
Chris@0 37 (typeof jQuery === 'undefined' || (jQuery.active === 0 && jQuery(':animated').length === 0)) &&
Chris@0 38 (typeof Drupal === 'undefined' || typeof Drupal.ajax === 'undefined' || !Drupal.ajax.instances.some(isAjaxing))
Chris@0 39 );
Chris@0 40 }());
Chris@0 41 JS;
Chris@0 42 $result = $this->session->wait($timeout, $condition);
Chris@0 43 if (!$result) {
Chris@0 44 throw new \RuntimeException($message);
Chris@0 45 }
Chris@0 46 }
Chris@0 47
Chris@0 48 /**
Chris@0 49 * Waits for the specified selector and returns it when available.
Chris@0 50 *
Chris@0 51 * @param string $selector
Chris@0 52 * The selector engine name. See ElementInterface::findAll() for the
Chris@0 53 * supported selectors.
Chris@0 54 * @param string|array $locator
Chris@0 55 * The selector locator.
Chris@0 56 * @param int $timeout
Chris@0 57 * (Optional) Timeout in milliseconds, defaults to 10000.
Chris@0 58 *
Chris@0 59 * @return \Behat\Mink\Element\NodeElement|null
Chris@0 60 * The page element node if found, NULL if not.
Chris@0 61 *
Chris@0 62 * @see \Behat\Mink\Element\ElementInterface::findAll()
Chris@0 63 */
Chris@0 64 public function waitForElement($selector, $locator, $timeout = 10000) {
Chris@0 65 $page = $this->session->getPage();
Chris@0 66
Chris@0 67 $result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
Chris@0 68 return $page->find($selector, $locator);
Chris@0 69 });
Chris@0 70
Chris@0 71 return $result;
Chris@0 72 }
Chris@0 73
Chris@0 74 /**
Chris@0 75 * Waits for the specified selector and returns it when available and visible.
Chris@0 76 *
Chris@0 77 * @param string $selector
Chris@0 78 * The selector engine name. See ElementInterface::findAll() for the
Chris@0 79 * supported selectors.
Chris@0 80 * @param string|array $locator
Chris@0 81 * The selector locator.
Chris@0 82 * @param int $timeout
Chris@0 83 * (Optional) Timeout in milliseconds, defaults to 10000.
Chris@0 84 *
Chris@0 85 * @return \Behat\Mink\Element\NodeElement|null
Chris@0 86 * The page element node if found and visible, NULL if not.
Chris@0 87 *
Chris@0 88 * @see \Behat\Mink\Element\ElementInterface::findAll()
Chris@0 89 */
Chris@0 90 public function waitForElementVisible($selector, $locator, $timeout = 10000) {
Chris@0 91 $page = $this->session->getPage();
Chris@0 92
Chris@0 93 $result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
Chris@0 94 $element = $page->find($selector, $locator);
Chris@0 95 if (!empty($element) && $element->isVisible()) {
Chris@0 96 return $element;
Chris@0 97 }
Chris@0 98 return NULL;
Chris@0 99 });
Chris@0 100
Chris@0 101 return $result;
Chris@0 102 }
Chris@17 103
Chris@17 104 /**
Chris@17 105 * Waits for the specified text and returns its element when available.
Chris@17 106 *
Chris@17 107 * @param string $text
Chris@17 108 * The text to wait for.
Chris@17 109 * @param int $timeout
Chris@17 110 * (Optional) Timeout in milliseconds, defaults to 10000.
Chris@17 111 *
Chris@17 112 * @return \Behat\Mink\Element\NodeElement|null
Chris@17 113 * The page element node if found and visible, NULL if not.
Chris@17 114 */
Chris@17 115 public function waitForText($text, $timeout = 10000) {
Chris@17 116 $page = $this->session->getPage();
Chris@17 117 return $page->waitFor($timeout / 1000, function () use ($page, $text) {
Chris@17 118 $actual = preg_replace('/\s+/u', ' ', $page->getText());
Chris@17 119 $regex = '/' . preg_quote($text, '/') . '/ui';
Chris@17 120 return (bool) preg_match($regex, $actual);
Chris@17 121 });
Chris@17 122 }
Chris@17 123
Chris@0 124 /**
Chris@0 125 * Waits for a button (input[type=submit|image|button|reset], button) with
Chris@0 126 * specified locator and returns it.
Chris@0 127 *
Chris@0 128 * @param string $locator
Chris@0 129 * The button ID, value or alt string.
Chris@0 130 * @param int $timeout
Chris@0 131 * (Optional) Timeout in milliseconds, defaults to 10000.
Chris@0 132 *
Chris@0 133 * @return \Behat\Mink\Element\NodeElement|null
Chris@0 134 * The page element node if found, NULL if not.
Chris@0 135 */
Chris@0 136 public function waitForButton($locator, $timeout = 10000) {
Chris@0 137 return $this->waitForElement('named', ['button', $locator], $timeout);
Chris@0 138 }
Chris@0 139
Chris@0 140 /**
Chris@0 141 * Waits for a link with specified locator and returns it when available.
Chris@0 142 *
Chris@0 143 * @param string $locator
Chris@0 144 * The link ID, title, text or image alt.
Chris@0 145 * @param int $timeout
Chris@0 146 * (Optional) Timeout in milliseconds, defaults to 10000.
Chris@0 147 *
Chris@0 148 * @return \Behat\Mink\Element\NodeElement|null
Chris@0 149 * The page element node if found, NULL if not.
Chris@0 150 */
Chris@0 151 public function waitForLink($locator, $timeout = 10000) {
Chris@0 152 return $this->waitForElement('named', ['link', $locator], $timeout);
Chris@0 153 }
Chris@0 154
Chris@0 155 /**
Chris@0 156 * Waits for a field with specified locator and returns it when available.
Chris@0 157 *
Chris@0 158 * @param string $locator
Chris@0 159 * The input ID, name or label for the field (input, textarea, select).
Chris@0 160 * @param int $timeout
Chris@0 161 * (Optional) Timeout in milliseconds, defaults to 10000.
Chris@0 162 *
Chris@0 163 * @return \Behat\Mink\Element\NodeElement|null
Chris@0 164 * The page element node if found, NULL if not.
Chris@0 165 */
Chris@0 166 public function waitForField($locator, $timeout = 10000) {
Chris@0 167 return $this->waitForElement('named', ['field', $locator], $timeout);
Chris@0 168 }
Chris@0 169
Chris@0 170 /**
Chris@0 171 * Waits for an element by its id and returns it when available.
Chris@0 172 *
Chris@0 173 * @param string $id
Chris@0 174 * The element ID.
Chris@0 175 * @param int $timeout
Chris@0 176 * (Optional) Timeout in milliseconds, defaults to 10000.
Chris@0 177 *
Chris@0 178 * @return \Behat\Mink\Element\NodeElement|null
Chris@0 179 * The page element node if found, NULL if not.
Chris@0 180 */
Chris@0 181 public function waitForId($id, $timeout = 10000) {
Chris@0 182 return $this->waitForElement('named', ['id', $id], $timeout);
Chris@0 183 }
Chris@0 184
Chris@0 185 /**
Chris@0 186 * Waits for the jQuery autocomplete delay duration.
Chris@0 187 *
Chris@0 188 * @see https://api.jqueryui.com/autocomplete/#option-delay
Chris@0 189 */
Chris@0 190 public function waitOnAutocomplete() {
Chris@0 191 // Wait for the autocomplete to be visible.
Chris@0 192 return $this->waitForElementVisible('css', '.ui-autocomplete li');
Chris@0 193 }
Chris@0 194
Chris@0 195 /**
Chris@14 196 * Test that a node, or its specific corner, is visible in the viewport.
Chris@0 197 *
Chris@0 198 * Note: Always set the viewport size. This can be done with a PhantomJS
Chris@0 199 * startup parameter or in your test with \Behat\Mink\Session->resizeWindow().
Chris@0 200 * Drupal CI Javascript tests by default use a viewport of 1024x768px.
Chris@0 201 *
Chris@0 202 * @param string $selector_type
Chris@0 203 * The element selector type (CSS, XPath).
Chris@0 204 * @param string|array $selector
Chris@0 205 * The element selector. Note: the first found element is used.
Chris@0 206 * @param bool|string $corner
Chris@0 207 * (Optional) The corner to test:
Chris@0 208 * topLeft, topRight, bottomRight, bottomLeft.
Chris@0 209 * Or FALSE to check the complete element (default).
Chris@0 210 * @param string $message
Chris@0 211 * (optional) A message for the exception.
Chris@0 212 *
Chris@0 213 * @throws \Behat\Mink\Exception\ElementHtmlException
Chris@0 214 * When the element doesn't exist.
Chris@0 215 * @throws \Behat\Mink\Exception\ElementNotFoundException
Chris@0 216 * When the element is not visible in the viewport.
Chris@0 217 */
Chris@0 218 public function assertVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is not visible in the viewport.') {
Chris@0 219 $node = $this->session->getPage()->find($selector_type, $selector);
Chris@0 220 if ($node === NULL) {
Chris@0 221 if (is_array($selector)) {
Chris@0 222 $selector = implode(' ', $selector);
Chris@0 223 }
Chris@0 224 throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector);
Chris@0 225 }
Chris@0 226
Chris@0 227 // Check if the node is visible on the page, which is a prerequisite of
Chris@0 228 // being visible in the viewport.
Chris@0 229 if (!$node->isVisible()) {
Chris@0 230 throw new ElementHtmlException($message, $this->session->getDriver(), $node);
Chris@0 231 }
Chris@0 232
Chris@0 233 $result = $this->checkNodeVisibilityInViewport($node, $corner);
Chris@0 234
Chris@0 235 if (!$result) {
Chris@0 236 throw new ElementHtmlException($message, $this->session->getDriver(), $node);
Chris@0 237 }
Chris@0 238 }
Chris@0 239
Chris@0 240 /**
Chris@0 241 * Test that a node, or its specific corner, is not visible in the viewport.
Chris@0 242 *
Chris@0 243 * Note: the node should exist in the page, otherwise this assertion fails.
Chris@0 244 *
Chris@0 245 * @param string $selector_type
Chris@0 246 * The element selector type (CSS, XPath).
Chris@0 247 * @param string|array $selector
Chris@0 248 * The element selector. Note: the first found element is used.
Chris@0 249 * @param bool|string $corner
Chris@0 250 * (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft.
Chris@0 251 * Or FALSE to check the complete element (default).
Chris@0 252 * @param string $message
Chris@0 253 * (optional) A message for the exception.
Chris@0 254 *
Chris@0 255 * @throws \Behat\Mink\Exception\ElementHtmlException
Chris@0 256 * When the element doesn't exist.
Chris@0 257 * @throws \Behat\Mink\Exception\ElementNotFoundException
Chris@0 258 * When the element is not visible in the viewport.
Chris@0 259 *
Chris@0 260 * @see \Drupal\FunctionalJavascriptTests\JSWebAssert::assertVisibleInViewport()
Chris@0 261 */
Chris@0 262 public function assertNotVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is visible in the viewport.') {
Chris@0 263 $node = $this->session->getPage()->find($selector_type, $selector);
Chris@0 264 if ($node === NULL) {
Chris@0 265 if (is_array($selector)) {
Chris@0 266 $selector = implode(' ', $selector);
Chris@0 267 }
Chris@0 268 throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector);
Chris@0 269 }
Chris@0 270
Chris@0 271 $result = $this->checkNodeVisibilityInViewport($node, $corner);
Chris@0 272
Chris@0 273 if ($result) {
Chris@0 274 throw new ElementHtmlException($message, $this->session->getDriver(), $node);
Chris@0 275 }
Chris@0 276 }
Chris@0 277
Chris@0 278 /**
Chris@14 279 * Check the visibility of a node, or its specific corner.
Chris@0 280 *
Chris@0 281 * @param \Behat\Mink\Element\NodeElement $node
Chris@0 282 * A valid node.
Chris@0 283 * @param bool|string $corner
Chris@0 284 * (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft.
Chris@0 285 * Or FALSE to check the complete element (default).
Chris@0 286 *
Chris@0 287 * @return bool
Chris@0 288 * Returns TRUE if the node is visible in the viewport, FALSE otherwise.
Chris@0 289 *
Chris@0 290 * @throws \Behat\Mink\Exception\UnsupportedDriverActionException
Chris@0 291 * When an invalid corner specification is given.
Chris@0 292 */
Chris@0 293 private function checkNodeVisibilityInViewport(NodeElement $node, $corner = FALSE) {
Chris@0 294 $xpath = $node->getXpath();
Chris@0 295
Chris@0 296 // Build the Javascript to test if the complete element or a specific corner
Chris@0 297 // is in the viewport.
Chris@0 298 switch ($corner) {
Chris@0 299 case 'topLeft':
Chris@0 300 $test_javascript_function = <<<JS
Chris@0 301 function t(r, lx, ly) {
Chris@0 302 return (
Chris@0 303 r.top >= 0 &&
Chris@0 304 r.top <= ly &&
Chris@0 305 r.left >= 0 &&
Chris@0 306 r.left <= lx
Chris@0 307 )
Chris@0 308 }
Chris@0 309 JS;
Chris@0 310 break;
Chris@0 311
Chris@0 312 case 'topRight':
Chris@0 313 $test_javascript_function = <<<JS
Chris@0 314 function t(r, lx, ly) {
Chris@0 315 return (
Chris@0 316 r.top >= 0 &&
Chris@0 317 r.top <= ly &&
Chris@0 318 r.right >= 0 &&
Chris@0 319 r.right <= lx
Chris@0 320 );
Chris@0 321 }
Chris@0 322 JS;
Chris@0 323 break;
Chris@0 324
Chris@0 325 case 'bottomRight':
Chris@0 326 $test_javascript_function = <<<JS
Chris@0 327 function t(r, lx, ly) {
Chris@0 328 return (
Chris@0 329 r.bottom >= 0 &&
Chris@0 330 r.bottom <= ly &&
Chris@0 331 r.right >= 0 &&
Chris@0 332 r.right <= lx
Chris@0 333 );
Chris@0 334 }
Chris@0 335 JS;
Chris@0 336 break;
Chris@0 337
Chris@0 338 case 'bottomLeft':
Chris@0 339 $test_javascript_function = <<<JS
Chris@0 340 function t(r, lx, ly) {
Chris@0 341 return (
Chris@0 342 r.bottom >= 0 &&
Chris@0 343 r.bottom <= ly &&
Chris@0 344 r.left >= 0 &&
Chris@0 345 r.left <= lx
Chris@0 346 );
Chris@0 347 }
Chris@0 348 JS;
Chris@0 349 break;
Chris@0 350
Chris@0 351 case FALSE:
Chris@0 352 $test_javascript_function = <<<JS
Chris@0 353 function t(r, lx, ly) {
Chris@0 354 return (
Chris@0 355 r.top >= 0 &&
Chris@0 356 r.left >= 0 &&
Chris@0 357 r.bottom <= ly &&
Chris@0 358 r.right <= lx
Chris@0 359 );
Chris@0 360 }
Chris@0 361 JS;
Chris@0 362 break;
Chris@0 363
Chris@0 364 // Throw an exception if an invalid corner parameter is given.
Chris@0 365 default:
Chris@0 366 throw new UnsupportedDriverActionException($corner, $this->session->getDriver());
Chris@0 367 }
Chris@0 368
Chris@0 369 // Build the full Javascript test. The shared logic gets the corner
Chris@0 370 // specific test logic injected.
Chris@0 371 $full_javascript_visibility_test = <<<JS
Chris@0 372 (function(t){
Chris@0 373 var w = window,
Chris@0 374 d = document,
Chris@0 375 e = d.documentElement,
Chris@0 376 n = d.evaluate("$xpath", d, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue,
Chris@0 377 r = n.getBoundingClientRect(),
Chris@0 378 lx = (w.innerWidth || e.clientWidth),
Chris@0 379 ly = (w.innerHeight || e.clientHeight);
Chris@0 380
Chris@0 381 return t(r, lx, ly);
Chris@0 382 }($test_javascript_function));
Chris@0 383 JS;
Chris@0 384
Chris@0 385 // Check the visibility by injecting and executing the full Javascript test
Chris@0 386 // script in the page.
Chris@0 387 return $this->session->evaluateScript($full_javascript_visibility_test);
Chris@0 388 }
Chris@0 389
Chris@0 390 }