JSWebAssert.php 12 KB

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