123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- <?php
- namespace Drupal\FunctionalJavascriptTests;
- use Behat\Mink\Element\NodeElement;
- use Behat\Mink\Exception\ElementHtmlException;
- use Behat\Mink\Exception\ElementNotFoundException;
- use Behat\Mink\Exception\UnsupportedDriverActionException;
- use Drupal\Tests\WebAssert;
- /**
- * Defines a class with methods for asserting presence of elements during tests.
- */
- class JSWebAssert extends WebAssert {
- /**
- * Waits for AJAX request to be completed.
- *
- * @param int $timeout
- * (Optional) Timeout in milliseconds, defaults to 10000.
- * @param string $message
- * (optional) A message for exception.
- *
- * @throws \RuntimeException
- * When the request is not completed. If left blank, a default message will
- * be displayed.
- */
- public function assertWaitOnAjaxRequest($timeout = 10000, $message = 'Unable to complete AJAX request.') {
- $condition = <<<JS
- (function() {
- function isAjaxing(instance) {
- return instance && instance.ajaxing === true;
- }
- return (
- // Assert no AJAX request is running (via jQuery or Drupal) and no
- // animation is running.
- (typeof jQuery === 'undefined' || (jQuery.active === 0 && jQuery(':animated').length === 0)) &&
- (typeof Drupal === 'undefined' || typeof Drupal.ajax === 'undefined' || !Drupal.ajax.instances.some(isAjaxing))
- );
- }());
- JS;
- $result = $this->session->wait($timeout, $condition);
- if (!$result) {
- throw new \RuntimeException($message);
- }
- }
- /**
- * Waits for the specified selector and returns it when available.
- *
- * @param string $selector
- * The selector engine name. See ElementInterface::findAll() for the
- * supported selectors.
- * @param string|array $locator
- * The selector locator.
- * @param int $timeout
- * (Optional) Timeout in milliseconds, defaults to 10000.
- *
- * @return \Behat\Mink\Element\NodeElement|null
- * The page element node if found, NULL if not.
- *
- * @see \Behat\Mink\Element\ElementInterface::findAll()
- */
- public function waitForElement($selector, $locator, $timeout = 10000) {
- $page = $this->session->getPage();
- $result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
- return $page->find($selector, $locator);
- });
- return $result;
- }
- /**
- * Looks for the specified selector and returns TRUE when it is unavailable.
- *
- * @param string $selector
- * The selector engine name. See ElementInterface::findAll() for the
- * supported selectors.
- * @param string|array $locator
- * The selector locator.
- * @param int $timeout
- * (Optional) Timeout in milliseconds, defaults to 10000.
- *
- * @return bool
- * TRUE if not found, FALSE if found.
- *
- * @see \Behat\Mink\Element\ElementInterface::findAll()
- */
- public function waitForElementRemoved($selector, $locator, $timeout = 10000) {
- $page = $this->session->getPage();
- $result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
- return !$page->find($selector, $locator);
- });
- return $result;
- }
- /**
- * Waits for the specified selector and returns it when available and visible.
- *
- * @param string $selector
- * The selector engine name. See ElementInterface::findAll() for the
- * supported selectors.
- * @param string|array $locator
- * The selector locator.
- * @param int $timeout
- * (Optional) Timeout in milliseconds, defaults to 10000.
- *
- * @return \Behat\Mink\Element\NodeElement|null
- * The page element node if found and visible, NULL if not.
- *
- * @see \Behat\Mink\Element\ElementInterface::findAll()
- */
- public function waitForElementVisible($selector, $locator, $timeout = 10000) {
- $page = $this->session->getPage();
- $result = $page->waitFor($timeout / 1000, function () use ($page, $selector, $locator) {
- $element = $page->find($selector, $locator);
- if (!empty($element) && $element->isVisible()) {
- return $element;
- }
- return NULL;
- });
- return $result;
- }
- /**
- * Waits for the specified text and returns its element when available.
- *
- * @param string $text
- * The text to wait for.
- * @param int $timeout
- * (Optional) Timeout in milliseconds, defaults to 10000.
- *
- * @return \Behat\Mink\Element\NodeElement|null
- * The page element node if found and visible, NULL if not.
- */
- public function waitForText($text, $timeout = 10000) {
- $page = $this->session->getPage();
- return $page->waitFor($timeout / 1000, function () use ($page, $text) {
- $actual = preg_replace('/\s+/u', ' ', $page->getText());
- $regex = '/' . preg_quote($text, '/') . '/ui';
- return (bool) preg_match($regex, $actual);
- });
- }
- /**
- * Waits for a button (input[type=submit|image|button|reset], button) with
- * specified locator and returns it.
- *
- * @param string $locator
- * The button ID, value or alt string.
- * @param int $timeout
- * (Optional) Timeout in milliseconds, defaults to 10000.
- *
- * @return \Behat\Mink\Element\NodeElement|null
- * The page element node if found, NULL if not.
- */
- public function waitForButton($locator, $timeout = 10000) {
- return $this->waitForElement('named', ['button', $locator], $timeout);
- }
- /**
- * Waits for a link with specified locator and returns it when available.
- *
- * @param string $locator
- * The link ID, title, text or image alt.
- * @param int $timeout
- * (Optional) Timeout in milliseconds, defaults to 10000.
- *
- * @return \Behat\Mink\Element\NodeElement|null
- * The page element node if found, NULL if not.
- */
- public function waitForLink($locator, $timeout = 10000) {
- return $this->waitForElement('named', ['link', $locator], $timeout);
- }
- /**
- * Waits for a field with specified locator and returns it when available.
- *
- * @param string $locator
- * The input ID, name or label for the field (input, textarea, select).
- * @param int $timeout
- * (Optional) Timeout in milliseconds, defaults to 10000.
- *
- * @return \Behat\Mink\Element\NodeElement|null
- * The page element node if found, NULL if not.
- */
- public function waitForField($locator, $timeout = 10000) {
- return $this->waitForElement('named', ['field', $locator], $timeout);
- }
- /**
- * Waits for an element by its id and returns it when available.
- *
- * @param string $id
- * The element ID.
- * @param int $timeout
- * (Optional) Timeout in milliseconds, defaults to 10000.
- *
- * @return \Behat\Mink\Element\NodeElement|null
- * The page element node if found, NULL if not.
- */
- public function waitForId($id, $timeout = 10000) {
- return $this->waitForElement('named', ['id', $id], $timeout);
- }
- /**
- * Waits for the jQuery autocomplete delay duration.
- *
- * @see https://api.jqueryui.com/autocomplete/#option-delay
- */
- public function waitOnAutocomplete() {
- // Wait for the autocomplete to be visible.
- return $this->waitForElementVisible('css', '.ui-autocomplete li');
- }
- /**
- * Test that a node, or its specific corner, is visible in the viewport.
- *
- * Note: Always set the viewport size. This can be done with a PhantomJS
- * startup parameter or in your test with \Behat\Mink\Session->resizeWindow().
- * Drupal CI Javascript tests by default use a viewport of 1024x768px.
- *
- * @param string $selector_type
- * The element selector type (CSS, XPath).
- * @param string|array $selector
- * The element selector. Note: the first found element is used.
- * @param bool|string $corner
- * (Optional) The corner to test:
- * topLeft, topRight, bottomRight, bottomLeft.
- * Or FALSE to check the complete element (default).
- * @param string $message
- * (optional) A message for the exception.
- *
- * @throws \Behat\Mink\Exception\ElementHtmlException
- * When the element doesn't exist.
- * @throws \Behat\Mink\Exception\ElementNotFoundException
- * When the element is not visible in the viewport.
- */
- public function assertVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is not visible in the viewport.') {
- $node = $this->session->getPage()->find($selector_type, $selector);
- if ($node === NULL) {
- if (is_array($selector)) {
- $selector = implode(' ', $selector);
- }
- throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector);
- }
- // Check if the node is visible on the page, which is a prerequisite of
- // being visible in the viewport.
- if (!$node->isVisible()) {
- throw new ElementHtmlException($message, $this->session->getDriver(), $node);
- }
- $result = $this->checkNodeVisibilityInViewport($node, $corner);
- if (!$result) {
- throw new ElementHtmlException($message, $this->session->getDriver(), $node);
- }
- }
- /**
- * Test that a node, or its specific corner, is not visible in the viewport.
- *
- * Note: the node should exist in the page, otherwise this assertion fails.
- *
- * @param string $selector_type
- * The element selector type (CSS, XPath).
- * @param string|array $selector
- * The element selector. Note: the first found element is used.
- * @param bool|string $corner
- * (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft.
- * Or FALSE to check the complete element (default).
- * @param string $message
- * (optional) A message for the exception.
- *
- * @throws \Behat\Mink\Exception\ElementHtmlException
- * When the element doesn't exist.
- * @throws \Behat\Mink\Exception\ElementNotFoundException
- * When the element is not visible in the viewport.
- *
- * @see \Drupal\FunctionalJavascriptTests\JSWebAssert::assertVisibleInViewport()
- */
- public function assertNotVisibleInViewport($selector_type, $selector, $corner = FALSE, $message = 'Element is visible in the viewport.') {
- $node = $this->session->getPage()->find($selector_type, $selector);
- if ($node === NULL) {
- if (is_array($selector)) {
- $selector = implode(' ', $selector);
- }
- throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector);
- }
- $result = $this->checkNodeVisibilityInViewport($node, $corner);
- if ($result) {
- throw new ElementHtmlException($message, $this->session->getDriver(), $node);
- }
- }
- /**
- * Check the visibility of a node, or its specific corner.
- *
- * @param \Behat\Mink\Element\NodeElement $node
- * A valid node.
- * @param bool|string $corner
- * (Optional) Corner to test: topLeft, topRight, bottomRight, bottomLeft.
- * Or FALSE to check the complete element (default).
- *
- * @return bool
- * Returns TRUE if the node is visible in the viewport, FALSE otherwise.
- *
- * @throws \Behat\Mink\Exception\UnsupportedDriverActionException
- * When an invalid corner specification is given.
- */
- private function checkNodeVisibilityInViewport(NodeElement $node, $corner = FALSE) {
- $xpath = $node->getXpath();
- // Build the Javascript to test if the complete element or a specific corner
- // is in the viewport.
- switch ($corner) {
- case 'topLeft':
- $test_javascript_function = <<<JS
- function t(r, lx, ly) {
- return (
- r.top >= 0 &&
- r.top <= ly &&
- r.left >= 0 &&
- r.left <= lx
- )
- }
- JS;
- break;
- case 'topRight':
- $test_javascript_function = <<<JS
- function t(r, lx, ly) {
- return (
- r.top >= 0 &&
- r.top <= ly &&
- r.right >= 0 &&
- r.right <= lx
- );
- }
- JS;
- break;
- case 'bottomRight':
- $test_javascript_function = <<<JS
- function t(r, lx, ly) {
- return (
- r.bottom >= 0 &&
- r.bottom <= ly &&
- r.right >= 0 &&
- r.right <= lx
- );
- }
- JS;
- break;
- case 'bottomLeft':
- $test_javascript_function = <<<JS
- function t(r, lx, ly) {
- return (
- r.bottom >= 0 &&
- r.bottom <= ly &&
- r.left >= 0 &&
- r.left <= lx
- );
- }
- JS;
- break;
- case FALSE:
- $test_javascript_function = <<<JS
- function t(r, lx, ly) {
- return (
- r.top >= 0 &&
- r.left >= 0 &&
- r.bottom <= ly &&
- r.right <= lx
- );
- }
- JS;
- break;
- // Throw an exception if an invalid corner parameter is given.
- default:
- throw new UnsupportedDriverActionException($corner, $this->session->getDriver());
- }
- // Build the full Javascript test. The shared logic gets the corner
- // specific test logic injected.
- $full_javascript_visibility_test = <<<JS
- (function(t){
- var w = window,
- d = document,
- e = d.documentElement,
- n = d.evaluate("$xpath", d, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue,
- r = n.getBoundingClientRect(),
- lx = (w.innerWidth || e.clientWidth),
- ly = (w.innerHeight || e.clientHeight);
- return t(r, lx, ly);
- }($test_javascript_function));
- JS;
- // Check the visibility by injecting and executing the full Javascript test
- // script in the page.
- return $this->session->evaluateScript($full_javascript_visibility_test);
- }
- /**
- * Passes if the raw text IS NOT found escaped on the loaded page.
- *
- * Raw text refers to the raw HTML that the page generated.
- *
- * @param string $raw
- * Raw (HTML) string to look for.
- */
- public function assertNoEscaped($raw) {
- $this->responseNotContains($this->escapeHtml($raw));
- }
- /**
- * Passes if the raw text IS found escaped on the loaded page.
- *
- * Raw text refers to the raw HTML that the page generated.
- *
- * @param string $raw
- * Raw (HTML) string to look for.
- */
- public function assertEscaped($raw) {
- $this->responseContains($this->escapeHtml($raw));
- }
- /**
- * Escapes HTML for testing.
- *
- * Drupal's Html::escape() uses the ENT_QUOTES flag with htmlspecialchars() to
- * escape both single and double quotes. With JavascriptTestBase testing the
- * browser is automatically converting " and ' to double and single
- * quotes respectively therefore we can not escape them when testing for
- * escaped HTML.
- *
- * @param $raw
- * The raw string to escape.
- *
- * @return string
- * The string with escaped HTML.
- *
- * @see Drupal\Component\Utility\Html::escape()
- */
- protected function escapeHtml($raw) {
- return htmlspecialchars($raw, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
- }
- /**
- * Asserts that no matching element exists on the page after a wait.
- *
- * @param string $selector_type
- * The element selector type (css, xpath).
- * @param string|array $selector
- * The element selector.
- * @param int $timeout
- * (optional) Timeout in milliseconds, defaults to 10000.
- * @param string $message
- * (optional) The exception message.
- *
- * @throws \Behat\Mink\Exception\ElementHtmlException
- * When an element still exists on the page.
- */
- public function assertNoElementAfterWait($selector_type, $selector, $timeout = 10000, $message = 'Element exists on the page.') {
- $start = microtime(TRUE);
- $end = $start + ($timeout / 1000);
- $page = $this->session->getPage();
- do {
- $node = $page->find($selector_type, $selector);
- if (empty($node)) {
- return;
- }
- usleep(100000);
- } while (microtime(TRUE) < $end);
- throw new ElementHtmlException($message, $this->session->getDriver(), $node);
- }
- }
|