JSWebAssert.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  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. * Looks for the specified selector and returns TRUE when it is unavailable.
  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 bool
  78. * TRUE if not found, FALSE if found.
  79. *
  80. * @see \Behat\Mink\Element\ElementInterface::findAll()
  81. */
  82. public function waitForElementRemoved($selector, $locator, $timeout = 10000) {
  83. $page = $this->session->getPage();
  84. $result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
  85. return !$page->find($selector, $locator);
  86. });
  87. return $result;
  88. }
  89. /**
  90. * Waits for the specified selector and returns it when available and visible.
  91. *
  92. * @param string $selector
  93. * The selector engine name. See ElementInterface::findAll() for the
  94. * supported selectors.
  95. * @param string|array $locator
  96. * The selector locator.
  97. * @param int $timeout
  98. * (Optional) Timeout in milliseconds, defaults to 10000.
  99. *
  100. * @return \Behat\Mink\Element\NodeElement|null
  101. * The page element node if found and visible, NULL if not.
  102. *
  103. * @see \Behat\Mink\Element\ElementInterface::findAll()
  104. */
  105. public function waitForElementVisible($selector, $locator, $timeout = 10000) {
  106. $page = $this->session->getPage();
  107. $result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
  108. $element = $page->find($selector, $locator);
  109. if (!empty($element) && $element->isVisible()) {
  110. return $element;
  111. }
  112. return NULL;
  113. });
  114. return $result;
  115. }
  116. /**
  117. * Waits for the specified text and returns its element when available.
  118. *
  119. * @param string $text
  120. * The text to wait for.
  121. * @param int $timeout
  122. * (Optional) Timeout in milliseconds, defaults to 10000.
  123. *
  124. * @return \Behat\Mink\Element\NodeElement|null
  125. * The page element node if found and visible, NULL if not.
  126. */
  127. public function waitForText($text, $timeout = 10000) {
  128. $page = $this->session->getPage();
  129. return $page->waitFor($timeout / 1000, function () use ($page, $text) {
  130. $actual = preg_replace('/\s+/u', ' ', $page->getText());
  131. $regex = '/' . preg_quote($text, '/') . '/ui';
  132. return (bool) preg_match($regex, $actual);
  133. });
  134. }
  135. /**
  136. * Waits for a button (input[type=submit|image|button|reset], button) with
  137. * specified locator and returns it.
  138. *
  139. * @param string $locator
  140. * The button ID, value or alt string.
  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 waitForButton($locator, $timeout = 10000) {
  148. return $this->waitForElement('named', ['button', $locator], $timeout);
  149. }
  150. /**
  151. * Waits for a link with specified locator and returns it when available.
  152. *
  153. * @param string $locator
  154. * The link ID, title, text or image alt.
  155. * @param int $timeout
  156. * (Optional) Timeout in milliseconds, defaults to 10000.
  157. *
  158. * @return \Behat\Mink\Element\NodeElement|null
  159. * The page element node if found, NULL if not.
  160. */
  161. public function waitForLink($locator, $timeout = 10000) {
  162. return $this->waitForElement('named', ['link', $locator], $timeout);
  163. }
  164. /**
  165. * Waits for a field with specified locator and returns it when available.
  166. *
  167. * @param string $locator
  168. * The input ID, name or label for the field (input, textarea, select).
  169. * @param int $timeout
  170. * (Optional) Timeout in milliseconds, defaults to 10000.
  171. *
  172. * @return \Behat\Mink\Element\NodeElement|null
  173. * The page element node if found, NULL if not.
  174. */
  175. public function waitForField($locator, $timeout = 10000) {
  176. return $this->waitForElement('named', ['field', $locator], $timeout);
  177. }
  178. /**
  179. * Waits for an element by its id and returns it when available.
  180. *
  181. * @param string $id
  182. * The element ID.
  183. * @param int $timeout
  184. * (Optional) Timeout in milliseconds, defaults to 10000.
  185. *
  186. * @return \Behat\Mink\Element\NodeElement|null
  187. * The page element node if found, NULL if not.
  188. */
  189. public function waitForId($id, $timeout = 10000) {
  190. return $this->waitForElement('named', ['id', $id], $timeout);
  191. }
  192. /**
  193. * Waits for the jQuery autocomplete delay duration.
  194. *
  195. * @see https://api.jqueryui.com/autocomplete/#option-delay
  196. */
  197. public function waitOnAutocomplete() {
  198. // Wait for the autocomplete to be visible.
  199. return $this->waitForElementVisible('css', '.ui-autocomplete li');
  200. }
  201. /**
  202. * Test that a node, or its specific corner, is visible in the viewport.
  203. *
  204. * Note: Always set the viewport size. This can be done with a PhantomJS
  205. * startup parameter or in your test with \Behat\Mink\Session->resizeWindow().
  206. * Drupal CI Javascript tests by default use a viewport of 1024x768px.
  207. *
  208. * @param string $selector_type
  209. * The element selector type (CSS, XPath).
  210. * @param string|array $selector
  211. * The element selector. Note: the first found element is used.
  212. * @param bool|string $corner
  213. * (Optional) The corner to test:
  214. * topLeft, topRight, bottomRight, bottomLeft.
  215. * Or FALSE to check the complete element (default).
  216. * @param string $message
  217. * (optional) A message for the exception.
  218. *
  219. * @throws \Behat\Mink\Exception\ElementHtmlException
  220. * When the element doesn't exist.
  221. * @throws \Behat\Mink\Exception\ElementNotFoundException
  222. * When the element is not visible in the viewport.
  223. */
  224. public function assertVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is not visible in the viewport.') {
  225. $node = $this->session->getPage()->find($selector_type, $selector);
  226. if ($node === NULL) {
  227. if (is_array($selector)) {
  228. $selector = implode(' ', $selector);
  229. }
  230. throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector);
  231. }
  232. // Check if the node is visible on the page, which is a prerequisite of
  233. // being visible in the viewport.
  234. if (!$node->isVisible()) {
  235. throw new ElementHtmlException($message, $this->session->getDriver(), $node);
  236. }
  237. $result = $this->checkNodeVisibilityInViewport($node, $corner);
  238. if (!$result) {
  239. throw new ElementHtmlException($message, $this->session->getDriver(), $node);
  240. }
  241. }
  242. /**
  243. * Test that a node, or its specific corner, is not visible in the viewport.
  244. *
  245. * Note: the node should exist in the page, otherwise this assertion fails.
  246. *
  247. * @param string $selector_type
  248. * The element selector type (CSS, XPath).
  249. * @param string|array $selector
  250. * The element selector. Note: the first found element is used.
  251. * @param bool|string $corner
  252. * (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft.
  253. * Or FALSE to check the complete element (default).
  254. * @param string $message
  255. * (optional) A message for the exception.
  256. *
  257. * @throws \Behat\Mink\Exception\ElementHtmlException
  258. * When the element doesn't exist.
  259. * @throws \Behat\Mink\Exception\ElementNotFoundException
  260. * When the element is not visible in the viewport.
  261. *
  262. * @see \Drupal\FunctionalJavascriptTests\JSWebAssert::assertVisibleInViewport()
  263. */
  264. public function assertNotVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is visible in the viewport.') {
  265. $node = $this->session->getPage()->find($selector_type, $selector);
  266. if ($node === NULL) {
  267. if (is_array($selector)) {
  268. $selector = implode(' ', $selector);
  269. }
  270. throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector);
  271. }
  272. $result = $this->checkNodeVisibilityInViewport($node, $corner);
  273. if ($result) {
  274. throw new ElementHtmlException($message, $this->session->getDriver(), $node);
  275. }
  276. }
  277. /**
  278. * Check the visibility of a node, or its specific corner.
  279. *
  280. * @param \Behat\Mink\Element\NodeElement $node
  281. * A valid node.
  282. * @param bool|string $corner
  283. * (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft.
  284. * Or FALSE to check the complete element (default).
  285. *
  286. * @return bool
  287. * Returns TRUE if the node is visible in the viewport, FALSE otherwise.
  288. *
  289. * @throws \Behat\Mink\Exception\UnsupportedDriverActionException
  290. * When an invalid corner specification is given.
  291. */
  292. private function checkNodeVisibilityInViewport(NodeElement $node, $corner = FALSE) {
  293. $xpath = $node->getXpath();
  294. // Build the Javascript to test if the complete element or a specific corner
  295. // is in the viewport.
  296. switch ($corner) {
  297. case 'topLeft':
  298. $test_javascript_function = <<<JS
  299. function t(r, lx, ly) {
  300. return (
  301. r.top >= 0 &&
  302. r.top <= ly &&
  303. r.left >= 0 &&
  304. r.left <= lx
  305. )
  306. }
  307. JS;
  308. break;
  309. case 'topRight':
  310. $test_javascript_function = <<<JS
  311. function t(r, lx, ly) {
  312. return (
  313. r.top >= 0 &&
  314. r.top <= ly &&
  315. r.right >= 0 &&
  316. r.right <= lx
  317. );
  318. }
  319. JS;
  320. break;
  321. case 'bottomRight':
  322. $test_javascript_function = <<<JS
  323. function t(r, lx, ly) {
  324. return (
  325. r.bottom >= 0 &&
  326. r.bottom <= ly &&
  327. r.right >= 0 &&
  328. r.right <= lx
  329. );
  330. }
  331. JS;
  332. break;
  333. case 'bottomLeft':
  334. $test_javascript_function = <<<JS
  335. function t(r, lx, ly) {
  336. return (
  337. r.bottom >= 0 &&
  338. r.bottom <= ly &&
  339. r.left >= 0 &&
  340. r.left <= lx
  341. );
  342. }
  343. JS;
  344. break;
  345. case FALSE:
  346. $test_javascript_function = <<<JS
  347. function t(r, lx, ly) {
  348. return (
  349. r.top >= 0 &&
  350. r.left >= 0 &&
  351. r.bottom <= ly &&
  352. r.right <= lx
  353. );
  354. }
  355. JS;
  356. break;
  357. // Throw an exception if an invalid corner parameter is given.
  358. default:
  359. throw new UnsupportedDriverActionException($corner, $this->session->getDriver());
  360. }
  361. // Build the full Javascript test. The shared logic gets the corner
  362. // specific test logic injected.
  363. $full_javascript_visibility_test = <<<JS
  364. (function(t){
  365. var w = window,
  366. d = document,
  367. e = d.documentElement,
  368. n = d.evaluate("$xpath", d, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue,
  369. r = n.getBoundingClientRect(),
  370. lx = (w.innerWidth || e.clientWidth),
  371. ly = (w.innerHeight || e.clientHeight);
  372. return t(r, lx, ly);
  373. }($test_javascript_function));
  374. JS;
  375. // Check the visibility by injecting and executing the full Javascript test
  376. // script in the page.
  377. return $this->session->evaluateScript($full_javascript_visibility_test);
  378. }
  379. /**
  380. * Passes if the raw text IS NOT found escaped on the loaded page.
  381. *
  382. * Raw text refers to the raw HTML that the page generated.
  383. *
  384. * @param string $raw
  385. * Raw (HTML) string to look for.
  386. */
  387. public function assertNoEscaped($raw) {
  388. $this->responseNotContains($this->escapeHtml($raw));
  389. }
  390. /**
  391. * Passes if the raw text IS found escaped on the loaded page.
  392. *
  393. * Raw text refers to the raw HTML that the page generated.
  394. *
  395. * @param string $raw
  396. * Raw (HTML) string to look for.
  397. */
  398. public function assertEscaped($raw) {
  399. $this->responseContains($this->escapeHtml($raw));
  400. }
  401. /**
  402. * Escapes HTML for testing.
  403. *
  404. * Drupal's Html::escape() uses the ENT_QUOTES flag with htmlspecialchars() to
  405. * escape both single and double quotes. With JavascriptTestBase testing the
  406. * browser is automatically converting &quot; and &#039; to double and single
  407. * quotes respectively therefore we can not escape them when testing for
  408. * escaped HTML.
  409. *
  410. * @param $raw
  411. * The raw string to escape.
  412. *
  413. * @return string
  414. * The string with escaped HTML.
  415. *
  416. * @see Drupal\Component\Utility\Html::escape()
  417. */
  418. protected function escapeHtml($raw) {
  419. return htmlspecialchars($raw, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
  420. }
  421. /**
  422. * Asserts that no matching element exists on the page after a wait.
  423. *
  424. * @param string $selector_type
  425. * The element selector type (css, xpath).
  426. * @param string|array $selector
  427. * The element selector.
  428. * @param int $timeout
  429. * (optional) Timeout in milliseconds, defaults to 10000.
  430. * @param string $message
  431. * (optional) The exception message.
  432. *
  433. * @throws \Behat\Mink\Exception\ElementHtmlException
  434. * When an element still exists on the page.
  435. */
  436. public function assertNoElementAfterWait($selector_type, $selector, $timeout = 10000, $message = 'Element exists on the page.') {
  437. $start = microtime(TRUE);
  438. $end = $start + ($timeout / 1000);
  439. $page = $this->session->getPage();
  440. do {
  441. $node = $page->find($selector_type, $selector);
  442. if (empty($node)) {
  443. return;
  444. }
  445. usleep(100000);
  446. } while (microtime(TRUE) < $end);
  447. throw new ElementHtmlException($message, $this->session->getDriver(), $node);
  448. }
  449. }