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