WebAssert.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. <?php
  2. namespace Drupal\Tests;
  3. use Behat\Mink\Exception\ExpectationException;
  4. use Behat\Mink\Exception\ResponseTextException;
  5. use Behat\Mink\WebAssert as MinkWebAssert;
  6. use Behat\Mink\Element\TraversableElement;
  7. use Behat\Mink\Exception\ElementNotFoundException;
  8. use Behat\Mink\Session;
  9. use Drupal\Component\Utility\Html;
  10. use Drupal\Core\Url;
  11. /**
  12. * Defines a class with methods for asserting presence of elements during tests.
  13. */
  14. class WebAssert extends MinkWebAssert {
  15. /**
  16. * The absolute URL of the site under test.
  17. *
  18. * @var string
  19. */
  20. protected $baseUrl = '';
  21. /**
  22. * Constructor.
  23. *
  24. * @param \Behat\Mink\Session $session
  25. * The Behat session object;
  26. * @param string $base_url
  27. * The base URL of the site under test.
  28. */
  29. public function __construct(Session $session, $base_url = '') {
  30. parent::__construct($session);
  31. $this->baseUrl = $base_url;
  32. }
  33. /**
  34. * {@inheritdoc}
  35. */
  36. protected function cleanUrl($url) {
  37. if ($url instanceof Url) {
  38. $url = $url->setAbsolute()->toString();
  39. }
  40. // Strip the base URL from the beginning for absolute URLs.
  41. if ($this->baseUrl !== '' && strpos($url, $this->baseUrl) === 0) {
  42. $url = substr($url, strlen($this->baseUrl));
  43. }
  44. // Make sure there is a forward slash at the beginning of relative URLs for
  45. // consistency.
  46. if (parse_url($url, PHP_URL_HOST) === NULL && strpos($url, '/') !== 0) {
  47. $url = "/$url";
  48. }
  49. return parent::cleanUrl($url);
  50. }
  51. /**
  52. * Checks that specific button exists on the current page.
  53. *
  54. * @param string $button
  55. * One of id|name|label|value for the button.
  56. * @param \Behat\Mink\Element\TraversableElement $container
  57. * (optional) The document to check against. Defaults to the current page.
  58. *
  59. * @return \Behat\Mink\Element\NodeElement
  60. * The matching element.
  61. *
  62. * @throws \Behat\Mink\Exception\ElementNotFoundException
  63. * When the element doesn't exist.
  64. */
  65. public function buttonExists($button, TraversableElement $container = NULL) {
  66. $container = $container ?: $this->session->getPage();
  67. $node = $container->findButton($button);
  68. if ($node === NULL) {
  69. throw new ElementNotFoundException($this->session->getDriver(), 'button', 'id|name|label|value', $button);
  70. }
  71. return $node;
  72. }
  73. /**
  74. * Checks that the specific button does NOT exist on the current page.
  75. *
  76. * @param string $button
  77. * One of id|name|label|value for the button.
  78. * @param \Behat\Mink\Element\TraversableElement $container
  79. * (optional) The document to check against. Defaults to the current page.
  80. *
  81. * @throws \Behat\Mink\Exception\ExpectationException
  82. * When the button exists.
  83. */
  84. public function buttonNotExists($button, TraversableElement $container = NULL) {
  85. $container = $container ?: $this->session->getPage();
  86. $node = $container->findButton($button);
  87. $this->assert(NULL === $node, sprintf('A button "%s" appears on this page, but it should not.', $button));
  88. }
  89. /**
  90. * Checks that specific select field exists on the current page.
  91. *
  92. * @param string $select
  93. * One of id|name|label|value for the select field.
  94. * @param \Behat\Mink\Element\TraversableElement $container
  95. * (optional) The document to check against. Defaults to the current page.
  96. *
  97. * @return \Behat\Mink\Element\NodeElement
  98. * The matching element
  99. *
  100. * @throws \Behat\Mink\Exception\ElementNotFoundException
  101. * When the element doesn't exist.
  102. */
  103. public function selectExists($select, TraversableElement $container = NULL) {
  104. $container = $container ?: $this->session->getPage();
  105. $node = $container->find('named', [
  106. 'select',
  107. $select,
  108. ]);
  109. if ($node === NULL) {
  110. throw new ElementNotFoundException($this->session->getDriver(), 'select', 'id|name|label|value', $select);
  111. }
  112. return $node;
  113. }
  114. /**
  115. * Checks that specific option in a select field exists on the current page.
  116. *
  117. * @param string $select
  118. * One of id|name|label|value for the select field.
  119. * @param string $option
  120. * The option value.
  121. * @param \Behat\Mink\Element\TraversableElement $container
  122. * (optional) The document to check against. Defaults to the current page.
  123. *
  124. * @return \Behat\Mink\Element\NodeElement
  125. * The matching option element
  126. *
  127. * @throws \Behat\Mink\Exception\ElementNotFoundException
  128. * When the element doesn't exist.
  129. */
  130. public function optionExists($select, $option, TraversableElement $container = NULL) {
  131. $container = $container ?: $this->session->getPage();
  132. $select_field = $container->find('named', [
  133. 'select',
  134. $select,
  135. ]);
  136. if ($select_field === NULL) {
  137. throw new ElementNotFoundException($this->session->getDriver(), 'select', 'id|name|label|value', $select);
  138. }
  139. $option_field = $select_field->find('named', ['option', $option]);
  140. if ($option_field === NULL) {
  141. throw new ElementNotFoundException($this->session->getDriver(), 'select', 'id|name|label|value', $option);
  142. }
  143. return $option_field;
  144. }
  145. /**
  146. * Checks that an option in a select field does NOT exist on the current page.
  147. *
  148. * @param string $select
  149. * One of id|name|label|value for the select field.
  150. * @param string $option
  151. * The option value that should not exist.
  152. * @param \Behat\Mink\Element\TraversableElement $container
  153. * (optional) The document to check against. Defaults to the current page.
  154. *
  155. * @throws \Behat\Mink\Exception\ElementNotFoundException
  156. * When the select element doesn't exist.
  157. */
  158. public function optionNotExists($select, $option, TraversableElement $container = NULL) {
  159. $container = $container ?: $this->session->getPage();
  160. $select_field = $container->find('named', [
  161. 'select',
  162. $select,
  163. ]);
  164. if ($select_field === NULL) {
  165. throw new ElementNotFoundException($this->session->getDriver(), 'select', 'id|name|label|value', $select);
  166. }
  167. $option_field = $select_field->find('named', ['option', $option]);
  168. $this->assert($option_field === NULL, sprintf('An option "%s" exists in select "%s", but it should not.', $option, $select));
  169. }
  170. /**
  171. * Pass if the page title is the given string.
  172. *
  173. * @param string $expected_title
  174. * The string the page title should be.
  175. *
  176. * @throws \Behat\Mink\Exception\ExpectationException
  177. * Thrown when element doesn't exist, or the title is a different one.
  178. */
  179. public function titleEquals($expected_title) {
  180. $title_element = $this->session->getPage()->find('css', 'title');
  181. if (!$title_element) {
  182. throw new ExpectationException('No title element found on the page', $this->session->getDriver());
  183. }
  184. $actual_title = $title_element->getText();
  185. $this->assert($expected_title === $actual_title, 'Title found');
  186. }
  187. /**
  188. * Passes if a link with the specified label is found.
  189. *
  190. * An optional link index may be passed.
  191. *
  192. * @param string $label
  193. * Text between the anchor tags.
  194. * @param int $index
  195. * Link position counting from zero.
  196. * @param string $message
  197. * (optional) A message to display with the assertion. Do not translate
  198. * messages: use strtr() to embed variables in the message text, not
  199. * t(). If left blank, a default message will be displayed.
  200. *
  201. * @throws \Behat\Mink\Exception\ExpectationException
  202. * Thrown when element doesn't exist, or the link label is a different one.
  203. */
  204. public function linkExists($label, $index = 0, $message = '') {
  205. $message = ($message ? $message : strtr('Link with label %label found.', ['%label' => $label]));
  206. $links = $this->session->getPage()->findAll('named', ['link', $label]);
  207. $this->assert(!empty($links[$index]), $message);
  208. }
  209. /**
  210. * Passes if a link with the exactly specified label is found.
  211. *
  212. * An optional link index may be passed.
  213. *
  214. * @param string $label
  215. * Text between the anchor tags.
  216. * @param int $index
  217. * Link position counting from zero.
  218. * @param string $message
  219. * (optional) A message to display with the assertion. Do not translate
  220. * messages: use strtr() to embed variables in the message text, not
  221. * t(). If left blank, a default message will be displayed.
  222. *
  223. * @throws \Behat\Mink\Exception\ExpectationException
  224. * Thrown when element doesn't exist, or the link label is a different one.
  225. */
  226. public function linkExistsExact($label, $index = 0, $message = '') {
  227. $message = ($message ? $message : strtr('Link with label %label found.', ['%label' => $label]));
  228. $links = $this->session->getPage()->findAll('named_exact', ['link', $label]);
  229. $this->assert(!empty($links[$index]), $message);
  230. }
  231. /**
  232. * Passes if a link with the specified label is not found.
  233. *
  234. * An optional link index may be passed.
  235. *
  236. * @param string $label
  237. * Text between the anchor tags.
  238. * @param string $message
  239. * (optional) A message to display with the assertion. Do not translate
  240. * messages: use strtr() to embed variables in the message text, not
  241. * t(). If left blank, a default message will be displayed.
  242. *
  243. * @throws \Behat\Mink\Exception\ExpectationException
  244. * Thrown when element doesn't exist, or the link label is a different one.
  245. */
  246. public function linkNotExists($label, $message = '') {
  247. $message = ($message ? $message : strtr('Link with label %label not found.', ['%label' => $label]));
  248. $links = $this->session->getPage()->findAll('named', ['link', $label]);
  249. $this->assert(empty($links), $message);
  250. }
  251. /**
  252. * Passes if a link with the exactly specified label is not found.
  253. *
  254. * An optional link index may be passed.
  255. *
  256. * @param string $label
  257. * Text between the anchor tags.
  258. * @param string $message
  259. * (optional) A message to display with the assertion. Do not translate
  260. * messages: use strtr() to embed variables in the message text, not
  261. * t(). If left blank, a default message will be displayed.
  262. *
  263. * @throws \Behat\Mink\Exception\ExpectationException
  264. * Thrown when element doesn't exist, or the link label is a different one.
  265. */
  266. public function linkNotExistsExact($label, $message = '') {
  267. $message = ($message ? $message : strtr('Link with label %label not found.', ['%label' => $label]));
  268. $links = $this->session->getPage()->findAll('named_exact', ['link', $label]);
  269. $this->assert(empty($links), $message);
  270. }
  271. /**
  272. * Passes if a link containing a given href (part) is found.
  273. *
  274. * @param string $href
  275. * The full or partial value of the 'href' attribute of the anchor tag.
  276. * @param int $index
  277. * Link position counting from zero.
  278. * @param string $message
  279. * (optional) A message to display with the assertion. Do not translate
  280. * messages: use \Drupal\Component\Render\FormattableMarkup to embed
  281. * variables in the message text, not t(). If left blank, a default message
  282. * will be displayed.
  283. *
  284. * @throws \Behat\Mink\Exception\ExpectationException
  285. * Thrown when element doesn't exist, or the link label is a different one.
  286. */
  287. public function linkByHrefExists($href, $index = 0, $message = '') {
  288. $xpath = $this->buildXPathQuery('//a[contains(@href, :href)]', [':href' => $href]);
  289. $message = ($message ? $message : strtr('Link containing href %href found.', ['%href' => $href]));
  290. $links = $this->session->getPage()->findAll('xpath', $xpath);
  291. $this->assert(!empty($links[$index]), $message);
  292. }
  293. /**
  294. * Passes if a link containing a given href (part) is not found.
  295. *
  296. * @param string $href
  297. * The full or partial value of the 'href' attribute of the anchor tag.
  298. * @param string $message
  299. * (optional) A message to display with the assertion. Do not translate
  300. * messages: use \Drupal\Component\Render\FormattableMarkup to embed
  301. * variables in the message text, not t(). If left blank, a default message
  302. * will be displayed.
  303. *
  304. * @throws \Behat\Mink\Exception\ExpectationException
  305. * Thrown when element doesn't exist, or the link label is a different one.
  306. */
  307. public function linkByHrefNotExists($href, $message = '') {
  308. $xpath = $this->buildXPathQuery('//a[contains(@href, :href)]', [':href' => $href]);
  309. $message = ($message ? $message : strtr('No link containing href %href found.', ['%href' => $href]));
  310. $links = $this->session->getPage()->findAll('xpath', $xpath);
  311. $this->assert(empty($links), $message);
  312. }
  313. /**
  314. * Builds an XPath query.
  315. *
  316. * Builds an XPath query by replacing placeholders in the query by the value
  317. * of the arguments.
  318. *
  319. * XPath 1.0 (the version supported by libxml2, the underlying XML library
  320. * used by PHP) doesn't support any form of quotation. This function
  321. * simplifies the building of XPath expression.
  322. *
  323. * @param string $xpath
  324. * An XPath query, possibly with placeholders in the form ':name'.
  325. * @param array $args
  326. * An array of arguments with keys in the form ':name' matching the
  327. * placeholders in the query. The values may be either strings or numeric
  328. * values.
  329. *
  330. * @return string
  331. * An XPath query with arguments replaced.
  332. */
  333. public function buildXPathQuery($xpath, array $args = []) {
  334. // Replace placeholders.
  335. foreach ($args as $placeholder => $value) {
  336. if (is_object($value)) {
  337. throw new \InvalidArgumentException('Just pass in scalar values for $args and remove all t() calls from your test.');
  338. }
  339. // XPath 1.0 doesn't support a way to escape single or double quotes in a
  340. // string literal. We split double quotes out of the string, and encode
  341. // them separately.
  342. if (is_string($value)) {
  343. // Explode the text at the quote characters.
  344. $parts = explode('"', $value);
  345. // Quote the parts.
  346. foreach ($parts as &$part) {
  347. $part = '"' . $part . '"';
  348. }
  349. // Return the string.
  350. $value = count($parts) > 1 ? 'concat(' . implode(', \'"\', ', $parts) . ')' : $parts[0];
  351. }
  352. // Use preg_replace_callback() instead of preg_replace() to prevent the
  353. // regular expression engine from trying to substitute backreferences.
  354. $replacement = function ($matches) use ($value) {
  355. return $value;
  356. };
  357. $xpath = preg_replace_callback('/' . preg_quote($placeholder) . '\b/', $replacement, $xpath);
  358. }
  359. return $xpath;
  360. }
  361. /**
  362. * Passes if the raw text IS NOT found escaped on the loaded page.
  363. *
  364. * Raw text refers to the raw HTML that the page generated.
  365. *
  366. * @param string $raw
  367. * Raw (HTML) string to look for.
  368. */
  369. public function assertNoEscaped($raw) {
  370. $this->responseNotContains(Html::escape($raw));
  371. }
  372. /**
  373. * Passes if the raw text IS found escaped on the loaded page.
  374. *
  375. * Raw text refers to the raw HTML that the page generated.
  376. *
  377. * @param string $raw
  378. * Raw (HTML) string to look for.
  379. */
  380. public function assertEscaped($raw) {
  381. $this->responseContains(Html::escape($raw));
  382. }
  383. /**
  384. * Checks that page HTML (response content) contains text.
  385. *
  386. * @param string|object $text
  387. * Text value. Any non-string value will be cast to string.
  388. *
  389. * @throws \Behat\Mink\Exception\ExpectationException
  390. */
  391. public function responseContains($text) {
  392. parent::responseContains((string) $text);
  393. }
  394. /**
  395. * Checks that page HTML (response content) does not contains text.
  396. *
  397. * @param string|object $text
  398. * Text value. Any non-string value will be cast to string.
  399. *
  400. * @throws \Behat\Mink\Exception\ExpectationException
  401. */
  402. public function responseNotContains($text) {
  403. parent::responseNotContains((string) $text);
  404. }
  405. /**
  406. * Asserts a condition.
  407. *
  408. * The parent method is overridden because it is a private method.
  409. *
  410. * @param bool $condition
  411. * The condition.
  412. * @param string $message
  413. * The success message.
  414. *
  415. * @throws \Behat\Mink\Exception\ExpectationException
  416. * When the condition is not fulfilled.
  417. */
  418. public function assert($condition, $message) {
  419. if ($condition) {
  420. return;
  421. }
  422. throw new ExpectationException($message, $this->session->getDriver());
  423. }
  424. /**
  425. * Checks that a given form field element is disabled.
  426. *
  427. * @param string $field
  428. * One of id|name|label|value for the field.
  429. * @param \Behat\Mink\Element\TraversableElement $container
  430. * (optional) The document to check against. Defaults to the current page.
  431. *
  432. * @return \Behat\Mink\Element\NodeElement
  433. * The matching element.
  434. *
  435. * @throws \Behat\Mink\Exception\ElementNotFoundException
  436. * @throws \Behat\Mink\Exception\ExpectationException
  437. */
  438. public function fieldDisabled($field, TraversableElement $container = NULL) {
  439. $container = $container ?: $this->session->getPage();
  440. $node = $container->findField($field);
  441. if ($node === NULL) {
  442. throw new ElementNotFoundException($this->session->getDriver(), 'field', 'id|name|label|value', $field);
  443. }
  444. if (!$node->hasAttribute('disabled')) {
  445. throw new ExpectationException("Field $field is disabled", $this->session->getDriver());
  446. }
  447. return $node;
  448. }
  449. /**
  450. * Checks that a given form field element is enabled.
  451. *
  452. * @param string $field
  453. * One of id|name|label|value for the field.
  454. * @param \Behat\Mink\Element\TraversableElement $container
  455. * (optional) The document to check against. Defaults to the current page.
  456. *
  457. * @return \Behat\Mink\Element\NodeElement
  458. * The matching element.
  459. *
  460. * @throws \Behat\Mink\Exception\ElementNotFoundException
  461. * @throws \Behat\Mink\Exception\ExpectationException
  462. */
  463. public function fieldEnabled($field, TraversableElement $container = NULL) {
  464. $container = $container ?: $this->session->getPage();
  465. $node = $container->findField($field);
  466. if ($node === NULL) {
  467. throw new ElementNotFoundException($this->session->getDriver(), 'field', 'id|name|label|value', $field);
  468. }
  469. if ($node->hasAttribute('disabled')) {
  470. throw new ExpectationException("Field $field is not enabled", $this->session->getDriver());
  471. }
  472. return $node;
  473. }
  474. /**
  475. * Checks that specific hidden field exists.
  476. *
  477. * @param string $field
  478. * One of id|name|value for the hidden field.
  479. * @param \Behat\Mink\Element\TraversableElement $container
  480. * (optional) The document to check against. Defaults to the current page.
  481. *
  482. * @return \Behat\Mink\Element\NodeElement
  483. * The matching element.
  484. *
  485. * @throws \Behat\Mink\Exception\ElementNotFoundException
  486. */
  487. public function hiddenFieldExists($field, TraversableElement $container = NULL) {
  488. $container = $container ?: $this->session->getPage();
  489. if ($node = $container->find('hidden_field_selector', ['hidden_field', $field])) {
  490. return $node;
  491. }
  492. throw new ElementNotFoundException($this->session->getDriver(), 'form hidden field', 'id|name|value', $field);
  493. }
  494. /**
  495. * Checks that specific hidden field does not exist.
  496. *
  497. * @param string $field
  498. * One of id|name|value for the hidden field.
  499. * @param \Behat\Mink\Element\TraversableElement $container
  500. * (optional) The document to check against. Defaults to the current page.
  501. *
  502. * @throws \Behat\Mink\Exception\ExpectationException
  503. */
  504. public function hiddenFieldNotExists($field, TraversableElement $container = NULL) {
  505. $container = $container ?: $this->session->getPage();
  506. $node = $container->find('hidden_field_selector', ['hidden_field', $field]);
  507. $this->assert($node === NULL, "A hidden field '$field' exists on this page, but it should not.");
  508. }
  509. /**
  510. * Checks that specific hidden field have provided value.
  511. *
  512. * @param string $field
  513. * One of id|name|value for the hidden field.
  514. * @param string $value
  515. * The hidden field value that needs to be checked.
  516. * @param \Behat\Mink\Element\TraversableElement $container
  517. * (optional) The document to check against. Defaults to the current page.
  518. *
  519. * @throws \Behat\Mink\Exception\ElementNotFoundException
  520. * @throws \Behat\Mink\Exception\ExpectationException
  521. */
  522. public function hiddenFieldValueEquals($field, $value, TraversableElement $container = NULL) {
  523. $node = $this->hiddenFieldExists($field, $container);
  524. $actual = $node->getValue();
  525. $regex = '/^' . preg_quote($value, '/') . '$/ui';
  526. $message = "The hidden field '$field' value is '$actual', but '$value' expected.";
  527. $this->assert((bool) preg_match($regex, $actual), $message);
  528. }
  529. /**
  530. * Checks that specific hidden field doesn't have the provided value.
  531. *
  532. * @param string $field
  533. * One of id|name|value for the hidden field.
  534. * @param string $value
  535. * The hidden field value that needs to be checked.
  536. * @param \Behat\Mink\Element\TraversableElement $container
  537. * (optional) The document to check against. Defaults to the current page.
  538. *
  539. * @throws \Behat\Mink\Exception\ElementNotFoundException
  540. * @throws \Behat\Mink\Exception\ExpectationException
  541. */
  542. public function hiddenFieldValueNotEquals($field, $value, TraversableElement $container = NULL) {
  543. $node = $this->hiddenFieldExists($field, $container);
  544. $actual = $node->getValue();
  545. $regex = '/^' . preg_quote($value, '/') . '$/ui';
  546. $message = "The hidden field '$field' value is '$actual', but it should not be.";
  547. $this->assert(!preg_match($regex, $actual), $message);
  548. }
  549. /**
  550. * Checks that current page contains text only once.
  551. *
  552. * @param string $text
  553. * The string to look for.
  554. *
  555. * @see \Behat\Mink\WebAssert::pageTextContains()
  556. */
  557. public function pageTextContainsOnce($text) {
  558. $actual = $this->session->getPage()->getText();
  559. $actual = preg_replace('/\s+/u', ' ', $actual);
  560. $regex = '/' . preg_quote($text, '/') . '/ui';
  561. $count = preg_match_all($regex, $actual);
  562. if ($count === 1) {
  563. return;
  564. }
  565. if ($count > 1) {
  566. $message = sprintf('The text "%s" appears in the text of this page more than once, but it should not.', $text);
  567. }
  568. else {
  569. $message = sprintf('The text "%s" was not found anywhere in the text of the current page.', $text);
  570. }
  571. throw new ResponseTextException($message, $this->session->getDriver());
  572. }
  573. }