WebAssert.php 19 KB

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