From 56c92d68123c6f5488bd072cf2fd14cd54b14e7b Mon Sep 17 00:00:00 2001 From: Tavo Nieves J Date: Sun, 11 Oct 2020 18:52:32 -0500 Subject: [PATCH 1/7] attributes were rearranged --- src/Codeception/Lib/InnerBrowser.php | 30 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Codeception/Lib/InnerBrowser.php b/src/Codeception/Lib/InnerBrowser.php index f0c52e5..ccfc86a 100644 --- a/src/Codeception/Lib/InnerBrowser.php +++ b/src/Codeception/Lib/InnerBrowser.php @@ -20,6 +20,7 @@ use Codeception\Util\Locator; use Codeception\Util\ReflectionHelper; use Codeception\Util\Uri; +use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\BrowserKit\Cookie; use Symfony\Component\BrowserKit\Exception\BadMethodCallException; use Symfony\Component\DomCrawler\Crawler; @@ -38,29 +39,34 @@ class_alias('Symfony\Component\BrowserKit\Client', 'Symfony\Component\BrowserKit class InnerBrowser extends Module implements Web, PageSourceSaver, ElementLocator, ConflictsWithModule { + private $baseUrl; + /** - * @var \Symfony\Component\DomCrawler\Crawler + * @var Crawler */ protected $crawler; - /** - * @api - * @var \Symfony\Component\BrowserKit\AbstractBrowser - */ - public $client; + protected $defaultCookieParameters = [ + 'expires' => null, + 'path' => '/', + 'domain' => '', + 'secure' => false + ]; /** - * @var array|\Symfony\Component\DomCrawler\Form[] + * @var array|Form[] */ protected $forms = []; - public $headers = []; - - protected $defaultCookieParameters = ['expires' => null, 'path' => '/', 'domain' => '', 'secure' => false]; - protected $internalDomains; - private $baseUrl; + /** + * @api + * @var AbstractBrowser + */ + public $client; + + public $headers = []; public function _failed(TestInterface $test, $fail) { From 532340bed5813c01079a614f9aa6c5ba237da91d Mon Sep 17 00:00:00 2001 From: Tavo Nieves J Date: Sun, 11 Oct 2020 19:05:10 -0500 Subject: [PATCH 2/7] private functions were rearranged --- src/Codeception/Lib/InnerBrowser.php | 318 ++++++++++++++------------- 1 file changed, 164 insertions(+), 154 deletions(-) diff --git a/src/Codeception/Lib/InnerBrowser.php b/src/Codeception/Lib/InnerBrowser.php index ccfc86a..390548c 100644 --- a/src/Codeception/Lib/InnerBrowser.php +++ b/src/Codeception/Lib/InnerBrowser.php @@ -20,6 +20,8 @@ use Codeception\Util\Locator; use Codeception\Util\ReflectionHelper; use Codeception\Util\Uri; +use DOMDocument; +use DOMNode; use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\BrowserKit\Cookie; use Symfony\Component\BrowserKit\Exception\BadMethodCallException; @@ -68,6 +70,168 @@ class InnerBrowser extends Module implements Web, PageSourceSaver, ElementLocato public $headers = []; + /** + * Clicks the link or submits the form when the button is clicked + * @param DOMNode $node + * @return boolean clicked something + * @throws ModuleException + */ + private function clickButton(DOMNode $node) + { + /** + * First we check if the button is associated to a form. + * It is associated to a form when it has a nonempty form + */ + $formAttribute = $node->attributes->getNamedItem('form'); + if (isset($formAttribute)) { + $form = empty($formAttribute->nodeValue) ? null : $this->filterByCSS('#' . $formAttribute->nodeValue)->getNode(0); + } else { + // Check parents + $currentNode = $node; + $form = null; + while ($currentNode->parentNode !== null) { + $currentNode = $currentNode->parentNode; + if ($currentNode->nodeName === 'form') { + $form = $node; + break; + } + } + } + + if (isset($form)) { + $buttonName = $node->getAttribute('name'); + if ($buttonName !== '') { + $formParams = [$buttonName => $node->getAttribute('value')]; + } else { + $formParams = []; + } + $this->proceedSubmitForm( + new Crawler($form, $this->getAbsoluteUrlFor($this->_getCurrentUri()), $this->getBaseUrl()), + $formParams + ); + return true; + } + + // Check if the button is inside an anchor. + $currentNode = $node; + while ($currentNode->parentNode !== null) { + $currentNode = $currentNode->parentNode; + if ($currentNode->nodeName === 'a') { + $this->openHrefFromDomNode($currentNode); + return true; + } + } + throw new TestRuntimeException('Button is not inside a link or a form'); + } + + private function getBaseUrl() + { + return $this->baseUrl; + } + + /** + * @return Crawler + * @throws ModuleException + */ + private function getCrawler() + { + if (!$this->crawler) { + throw new ModuleException($this, 'Crawler is null. Perhaps you forgot to call "amOnPage"?'); + } + return $this->crawler; + } + + /** + * Returns a crawler Form object for the form pointed to by the + * passed Crawler. + * + * The returned form is an independent Crawler created to take care + * of the following issues currently experienced by Crawler's form + * object: + * - input fields disabled at a higher level (e.g. by a surrounding + * fieldset) still return values + * - Codeception expects an empty value to match an unselected + * select box. + * + * The function clones the crawler's node and creates a new crawler + * because it destroys or adds to the DOM for the form to achieve + * the desired functionality. Other functions simply querying the + * DOM wouldn't expect them. + * + * @param Crawler $form the form + * @return Form + */ + private function getFormFromCrawler(Crawler $form) + { + $fakeDom = new DOMDocument(); + $fakeDom->appendChild($fakeDom->importNode($form->getNode(0), true)); + $node = $fakeDom->documentElement; + $action = (string)$this->getFormUrl($form); + $cloned = new Crawler($node, $action, $this->getBaseUrl()); + $shouldDisable = $cloned->filter( + 'input:disabled:not([disabled]),select option:disabled,select optgroup:disabled option:not([disabled]),textarea:disabled:not([disabled]),select:disabled:not([disabled])' + ); + foreach ($shouldDisable as $field) { + $field->parentNode->removeChild($field); + } + return $cloned->form(); + } + + /** + * @return AbstractBrowser + * @throws ModuleException + */ + private function getRunningClient() + { + try { + if ($this->client->getInternalRequest() === null) { + throw new ModuleException( + $this, + "Page not loaded. Use `\$I->amOnPage` (or hidden API methods `_request` and `_loadPage`) to open it" + ); + } + } catch (BadMethodCallException $e) { + //Symfony 5 + throw new ModuleException( + $this, + "Page not loaded. Use `\$I->amOnPage` (or hidden API methods `_request` and `_loadPage`) to open it" + ); + } + return $this->client; + } + + private function openHrefFromDomNode(DOMNode $node) + { + $link = new Link($node, $this->getBaseUrl()); + $this->amOnPage(preg_replace('/#.*/', '', $link->getUri())); + } + + /** + * @return string + * @throws ModuleException + */ + private function retrieveBaseUrl() + { + $baseUrl = ''; + + $baseHref = $this->crawler->filter('base'); + if (count($baseHref) > 0) { + $baseUrl = $baseHref->getNode(0)->getAttribute('href'); + } + if ($baseUrl === '') { + $baseUrl = $this->_getCurrentUri(); + } + return $this->getAbsoluteUrlFor($baseUrl); + } + + private function stringifySelector($selector) + { + if (is_array($selector)) { + return trim(json_encode($selector), '{}'); + } + return $selector; + } + public function _failed(TestInterface $test, $fail) { try { @@ -289,37 +453,6 @@ public function _loadPage( $this->forms = []; } - /** - * @return Crawler - * @throws ModuleException - */ - private function getCrawler() - { - if (!$this->crawler) { - throw new ModuleException($this, 'Crawler is null. Perhaps you forgot to call "amOnPage"?'); - } - return $this->crawler; - } - - private function getRunningClient() - { - try { - if ($this->client->getInternalRequest() === null) { - throw new ModuleException( - $this, - "Page not loaded. Use `\$I->amOnPage` (or hidden API methods `_request` and `_loadPage`) to open it" - ); - } - } catch (BadMethodCallException $e) { - //Symfony 5 - throw new ModuleException( - $this, - "Page not loaded. Use `\$I->amOnPage` (or hidden API methods `_request` and `_loadPage`) to open it" - ); - } - return $this->client; - } - public function _savePageSource($filename) { file_put_contents($filename, $this->_getResponseContent()); @@ -459,85 +592,6 @@ protected function clickByLocator($link) } } - - /** - * Clicks the link or submits the form when the button is clicked - * @param \DOMNode $node - * @return boolean clicked something - */ - private function clickButton(\DOMNode $node) - { - /** - * First we check if the button is associated to a form. - * It is associated to a form when it has a nonempty form - */ - $formAttribute = $node->attributes->getNamedItem('form'); - if (isset($formAttribute)) { - $form = empty($formAttribute->nodeValue) ? null : $this->filterByCSS('#' . $formAttribute->nodeValue)->getNode(0); - } else { - // Check parents - $currentNode = $node; - $form = null; - while ($currentNode->parentNode !== null) { - $currentNode = $currentNode->parentNode; - if ($currentNode->nodeName === 'form') { - $form = $node; - break; - } - } - } - - if (isset($form)) { - $buttonName = $node->getAttribute('name'); - if ($buttonName !== '') { - $formParams = [$buttonName => $node->getAttribute('value')]; - } else { - $formParams = []; - } - $this->proceedSubmitForm( - new Crawler($form, $this->getAbsoluteUrlFor($this->_getCurrentUri()), $this->getBaseUrl()), - $formParams - ); - return true; - } - - // Check if the button is inside an anchor. - $currentNode = $node; - while ($currentNode->parentNode !== null) { - $currentNode = $currentNode->parentNode; - if ($currentNode->nodeName === 'a') { - $this->openHrefFromDomNode($currentNode); - return true; - } - } - throw new TestRuntimeException('Button is not inside a link or a form'); - } - - private function openHrefFromDomNode(\DOMNode $node) - { - $link = new Link($node, $this->getBaseUrl()); - $this->amOnPage(preg_replace('/#.*/', '', $link->getUri())); - } - - private function getBaseUrl() - { - return $this->baseUrl; - } - - private function retrieveBaseUrl() - { - $baseUrl = ''; - - $baseHref = $this->crawler->filter('base'); - if (count($baseHref) > 0) { - $baseUrl = $baseHref->getNode(0)->getAttribute('href'); - } - if ($baseUrl === '') { - $baseUrl = $this->_getCurrentUri(); - } - return $this->getAbsoluteUrlFor($baseUrl); - } - public function see($text, $selector = null) { if (!$selector) { @@ -985,42 +1039,6 @@ protected function getFormUrl(Crawler $form) return $this->getAbsoluteUrlFor($action); } - /** - * Returns a crawler Form object for the form pointed to by the - * passed Crawler. - * - * The returned form is an independent Crawler created to take care - * of the following issues currently experienced by Crawler's form - * object: - * - input fields disabled at a higher level (e.g. by a surrounding - * fieldset) still return values - * - Codeception expects an empty value to match an unselected - * select box. - * - * The function clones the crawler's node and creates a new crawler - * because it destroys or adds to the DOM for the form to achieve - * the desired functionality. Other functions simply querying the - * DOM wouldn't expect them. - * - * @param Crawler $form the form - * @return Form - */ - private function getFormFromCrawler(Crawler $form) - { - $fakeDom = new \DOMDocument(); - $fakeDom->appendChild($fakeDom->importNode($form->getNode(0), true)); - $node = $fakeDom->documentElement; - $action = (string)$this->getFormUrl($form); - $cloned = new Crawler($node, $action, $this->getBaseUrl()); - $shouldDisable = $cloned->filter( - 'input:disabled:not([disabled]),select option:disabled,select optgroup:disabled option:not([disabled]),textarea:disabled:not([disabled]),select:disabled:not([disabled])' - ); - foreach ($shouldDisable as $field) { - $field->parentNode->removeChild($field); - } - return $cloned->form(); - } - /** * Returns the DomCrawler\Form object for the form pointed to by * $node or its closes form parent. @@ -1561,14 +1579,6 @@ public function resetCookie($name, array $params = []) $this->debugCookieJar(); } - private function stringifySelector($selector) - { - if (is_array($selector)) { - return trim(json_encode($selector), '{}'); - } - return $selector; - } - public function seeElement($selector, $attributes = []) { $nodes = $this->match($selector); From f79471d9849e8cdf6a3dbd4df9bc14a935deb024 Mon Sep 17 00:00:00 2001 From: Tavo Nieves J Date: Sun, 11 Oct 2020 19:21:25 -0500 Subject: [PATCH 3/7] protected functions were rearranged --- src/Codeception/Lib/InnerBrowser.php | 2105 +++++++++++++------------- 1 file changed, 1079 insertions(+), 1026 deletions(-) diff --git a/src/Codeception/Lib/InnerBrowser.php b/src/Codeception/Lib/InnerBrowser.php index 390548c..04952ff 100644 --- a/src/Codeception/Lib/InnerBrowser.php +++ b/src/Codeception/Lib/InnerBrowser.php @@ -22,6 +22,8 @@ use Codeception\Util\Uri; use DOMDocument; use DOMNode; +use InvalidArgumentException; +use LogicException; use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\BrowserKit\Cookie; use Symfony\Component\BrowserKit\Exception\BadMethodCallException; @@ -232,128 +234,16 @@ private function stringifySelector($selector) return $selector; } - public function _failed(TestInterface $test, $fail) - { - try { - if (!$this->client || !$this->client->getInternalResponse()) { - return; - } - } catch (BadMethodCallException $e) { - //Symfony 5 throws exception if request() method threw an exception. - //The "request()" method must be called before "Symfony\Component\BrowserKit\AbstractBrowser::getInternalResponse()" - return; - } - $filename = preg_replace('~\W~', '.', Descriptor::getTestSignatureUnique($test)); - - $extensions = [ - 'application/json' => 'json', - 'text/xml' => 'xml', - 'application/xml' => 'xml', - 'text/plain' => 'txt' - ]; - - try { - $internalResponse = $this->client->getInternalResponse(); - } catch (BadMethodCallException $e) { - $internalResponse = false; - } - - $responseContentType = $internalResponse ? $internalResponse->getHeader('content-type') : ''; - list($responseMimeType) = explode(';', $responseContentType); - - $extension = isset($extensions[$responseMimeType]) ? $extensions[$responseMimeType] : 'html'; - - $filename = mb_strcut($filename, 0, 244, 'utf-8') . '.fail.' . $extension; - $this->_savePageSource($report = codecept_output_dir() . $filename); - $test->getMetadata()->addReport('html', $report); - $test->getMetadata()->addReport('response', $report); - } - - public function _after(TestInterface $test) - { - $this->client = null; - $this->crawler = null; - $this->forms = []; - $this->headers = []; - } - - public function _conflicts() - { - return 'Codeception\Lib\Interfaces\Web'; - } - - public function _findElements($locator) - { - return $this->match($locator); - } - - /** - * Send custom request to a backend using method, uri, parameters, etc. - * Use it in Helpers to create special request actions, like accessing API - * Returns a string with response body. - * - * ```php - * getModule('{{MODULE_NAME}}')->_request('POST', '/api/v1/users', ['name' => $name]); - * $user = json_decode($userData); - * return $user->id; - * } - * ?> - * ``` - * Does not load the response into the module so you can't interact with response page (click, fill forms). - * To load arbitrary page for interaction, use `_loadPage` method. - * - * @api - * @param $method - * @param $uri - * @param array $parameters - * @param array $files - * @param array $server - * @param null $content - * @return mixed|Crawler - * @throws ExternalUrlException - * @see `_loadPage` - */ - public function _request( + protected function clientRequest( $method, $uri, array $parameters = [], array $files = [], array $server = [], - $content = null + $content = null, + $changeHistory = true ) { - $this->clientRequest($method, $uri, $parameters, $files, $server, $content, true); - return $this->_getResponseContent(); - } - - /** - * Returns content of the last response - * Use it in Helpers when you want to retrieve response of request performed by another module. - * - * ```php - * assertStringContainsString($text, $this->getModule('{{MODULE_NAME}}')->_getResponseContent(), "response contains"); - * } - * ?> - * ``` - * - * @api - * @return string - * @throws ModuleException - */ - public function _getResponseContent() - { - return (string)$this->getRunningClient()->getInternalResponse()->getContent(); - } - - protected function clientRequest($method, $uri, array $parameters = [], array $files = [], array $server = [], $content = null, $changeHistory = true) - { - $this->debugSection("Request Headers", $this->headers); + $this->debugSection('Request Headers', $this->headers); foreach ($this->headers as $header => $val) { // moved from REST module @@ -389,8 +279,12 @@ protected function clientRequest($method, $uri, array $parameters = [], array $f $maxRedirects = $this->client->getMaxRedirects(); } else { //Symfony 2.7 support - $isFollowingRedirects = ReflectionHelper::readPrivateProperty($this->client, 'followRedirects', 'Symfony\Component\BrowserKit\Client'); - $maxRedirects = ReflectionHelper::readPrivateProperty($this->client, 'maxRedirects', 'Symfony\Component\BrowserKit\Client'); + $isFollowingRedirects = ReflectionHelper::readPrivateProperty( + $this->client, 'followRedirects', 'Symfony\Component\BrowserKit\Client' + ); + $maxRedirects = ReflectionHelper::readPrivateProperty( + $this->client, 'maxRedirects', 'Symfony\Component\BrowserKit\Client' + ); } if (!$isFollowingRedirects) { @@ -405,170 +299,72 @@ protected function clientRequest($method, $uri, array $parameters = [], array $f return $this->redirectIfNecessary($result, $maxRedirects, 0); } - protected function isInternalDomain($domain) - { - if ($this->internalDomains === null) { - $this->internalDomains = $this->getInternalDomains(); - } - - foreach ($this->internalDomains as $pattern) { - if (preg_match($pattern, $domain)) { - return true; - } - } - return false; - } - - /** - * Opens a page with arbitrary request parameters. - * Useful for testing multi-step forms on a specific step. - * - * ```php - * getModule('{{MODULE_NAME}}')->_loadPage('POST', '/checkout/step2', ['order' => $orderId]); - * } - * ?> - * ``` - * - * @api - * @param $method - * @param $uri - * @param array $parameters - * @param array $files - * @param array $server - * @param null $content - */ - public function _loadPage( - $method, - $uri, - array $parameters = [], - array $files = [], - array $server = [], - $content = null - ) { - $this->crawler = $this->clientRequest($method, $uri, $parameters, $files, $server, $content); - $this->baseUrl = $this->retrieveBaseUrl(); - $this->forms = []; - } - - public function _savePageSource($filename) + protected function assertDomContains($nodes, $message, $text = '') { - file_put_contents($filename, $this->_getResponseContent()); + $constraint = new CrawlerConstraint($text, $this->_getCurrentUri()); + $this->assertThat($nodes, $constraint, $message); } - /** - * Authenticates user for HTTP_AUTH - * - * @param $username - * @param $password - */ - public function amHttpAuthenticated($username, $password) + protected function assertDomNotContains($nodes, $message, $text = '') { - $this->client->setServerParameter('PHP_AUTH_USER', $username); - $this->client->setServerParameter('PHP_AUTH_PW', $password); + $constraint = new CrawlerNotConstraint($text, $this->_getCurrentUri()); + $this->assertThat($nodes, $constraint, $message); } /** - * Sets the HTTP header to the passed value - which is used on - * subsequent HTTP requests through PhpBrowser. - * - * Example: - * ```php - * haveHttpHeader('X-Requested-With', 'Codeception'); - * $I->amOnPage('test-headers.php'); - * ?> - * ``` - * - * To use special chars in Header Key use HTML Character Entities: - * Example: - * Header with underscore - 'Client_Id' - * should be represented as - 'Client_Id' or 'Client_Id' - * - * ```php - * haveHttpHeader('Client_Id', 'Codeception'); - * ?> - * ``` - * - * @param string $name the name of the request header - * @param string $value the value to set it to for subsequent - * requests + * @param $needle + * @param string $message + * @throws ModuleException */ - public function haveHttpHeader($name, $value) + protected function assertPageContains($needle, $message = '') { - $name = implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $name))))); - $this->headers[$name] = $value; + $constraint = new PageConstraint($needle, $this->_getCurrentUri()); + $this->assertThat( + $this->getNormalizedResponseContent(), + $constraint, + $message + ); } /** - * Deletes the header with the passed name. Subsequent requests - * will not have the deleted header in its request. - * - * Example: - * ```php - * haveHttpHeader('X-Requested-With', 'Codeception'); - * $I->amOnPage('test-headers.php'); - * // ... - * $I->deleteHeader('X-Requested-With'); - * $I->amOnPage('some-other-page.php'); - * ?> - * ``` - * - * @param string $name the name of the header to delete. + * @param $needle + * @param string $message + * @throws ModuleException */ - public function deleteHeader($name) + protected function assertPageNotContains($needle, $message = '') { - $name = implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $name))))); - unset($this->headers[$name]); + $constraint = new PageConstraint($needle, $this->_getCurrentUri()); + $this->assertThatItsNot( + $this->getNormalizedResponseContent(), + $constraint, + $message + ); } - - public function amOnPage($page) + protected function assertPageSourceContains($needle, $message = '') { - $this->_loadPage('GET', $page); + $constraint = new PageConstraint($needle, $this->_getCurrentUri()); + $this->assertThat( + $this->_getResponseContent(), + $constraint, + $message + ); } - public function click($link, $context = null) + protected function assertPageSourceNotContains($needle, $message = '') { - if ($context) { - $this->crawler = $this->match($context); - } - - if (is_array($link)) { - $this->clickByLocator($link); - return; - } - - $anchor = $this->strictMatch(['link' => $link]); - if (!count($anchor)) { - $anchor = $this->getCrawler()->selectLink($link); - } - if (count($anchor)) { - $this->openHrefFromDomNode($anchor->getNode(0)); - return; - } - - $buttonText = str_replace('"', "'", $link); - $button = $this->crawler->selectButton($buttonText); - - if (count($button) && $this->clickButton($button->getNode(0))) { - return; - } - - try { - $this->clickByLocator($link); - } catch (MalformedLocatorException $e) { - throw new ElementNotFound("name=$link", "'$link' is invalid CSS and XPath selector and Link or Button"); - } + $constraint = new PageConstraint($needle, $this->_getCurrentUri()); + $this->assertThatItsNot( + $this->_getResponseContent(), + $constraint, + $message + ); } /** * @param $link * @return bool + * @throws ModuleException */ protected function clickByLocator($link) { @@ -592,235 +388,285 @@ protected function clickByLocator($link) } } - public function see($text, $selector = null) + protected function debugCookieJar() { - if (!$selector) { - $this->assertPageContains($text); - return; - } - - $nodes = $this->match($selector); - $this->assertDomContains($nodes, $this->stringifySelector($selector), $text); + $cookies = $this->client->getCookieJar()->all(); + $cookieStrings = array_map('strval', $cookies); + $this->debugSection('Cookie Jar', $cookieStrings); } - public function dontSee($text, $selector = null) + /** + * @param $url + * @throws ModuleException + */ + protected function debugResponse($url) { - if (!$selector) { - $this->assertPageNotContains($text); - return; - } - - $nodes = $this->match($selector); - $this->assertDomNotContains($nodes, $this->stringifySelector($selector), $text); + $this->debugSection('Page', $url); + $this->debugSection('Response', $this->getResponseStatusCode()); + $this->debugSection('Request Cookies', $this->getRunningClient()->getInternalRequest()->getCookies()); + $this->debugSection('Response Headers', $this->getRunningClient()->getInternalResponse()->getHeaders()); } - public function seeInSource($raw) + protected function filterByAttributes(Crawler $nodes, array $attributes) { - $this->assertPageSourceContains($raw); + foreach ($attributes as $attr => $val) { + $nodes = $nodes->reduce( + static function (Crawler $node) use ($attr, $val) { + return $node->attr($attr) === $val; + } + ); + } + return $nodes; } - public function dontSeeInSource($raw) + /** + * @param $locator + * @return Crawler + * @throws ModuleException + */ + protected function filterByCSS($locator) { - $this->assertPageSourceNotContains($raw); + if (!Locator::isCSS($locator)) { + throw new MalformedLocatorException($locator, 'css'); + } + return $this->getCrawler()->filter($locator); } - public function seeLink($text, $url = null) + /** + * @param $locator + * @return Crawler + * @throws ModuleException + */ + protected function filterByXPath($locator) { - $crawler = $this->getCrawler()->selectLink($text); - if ($crawler->count() === 0) { - $this->fail("No links containing text '$text' were found in page " . $this->_getCurrentUri()); - } - if ($url) { - $crawler = $crawler->filterXPath(sprintf('.//a[substring(@href, string-length(@href) - string-length(%1$s) + 1)=%1$s]', Crawler::xpathLiteral($url))); - if ($crawler->count() === 0) { - $this->fail("No links containing text '$text' and URL '$url' were found in page " . $this->_getCurrentUri()); - } + if (!Locator::isXPath($locator)) { + throw new MalformedLocatorException($locator, 'xpath'); } - $this->assertTrue(true); + return $this->getCrawler()->filterXPath($locator); } - public function dontSeeLink($text, $url = '') + /** + * Returns an absolute URL for the passed URI with the current URL + * as the base path. + * + * @param string $uri the absolute or relative URI + * @return string the absolute URL + * @throws TestRuntimeException|ModuleException if either the current + * URL or the passed URI can't be parsed + */ + protected function getAbsoluteUrlFor($uri) { - $crawler = $this->getCrawler()->selectLink($text); - if (!$url && $crawler->count() > 0) { - $this->fail("Link containing text '$text' was found in page " . $this->_getCurrentUri()); - } - $crawler = $crawler->filterXPath( - sprintf('.//a[substring(@href, string-length(@href) - string-length(%1$s) + 1)=%1$s]', - Crawler::xpathLiteral($url)) - ); - if ($crawler->count() > 0) { - $this->fail("Link containing text '$text' and URL '$url' was found in page " . $this->_getCurrentUri()); + $currentUrl = $this->getRunningClient()->getHistory()->current()->getUri(); + if (empty($uri) || strpos($uri, '#') === 0) { + return $currentUrl; } + return Uri::mergeUrls($currentUrl, $uri); } /** - * @return string + * @param $field + * @return object|Crawler * @throws ModuleException */ - public function _getCurrentUri() + protected function getFieldByLabelOrCss($field) { - return Uri::retrieveUri($this->getRunningClient()->getHistory()->current()->getUri()); + $input = $this->getFieldsByLabelOrCss($field); + return $input->first(); } - public function seeInCurrentUrl($uri) + /** + * @param $field + * + * @return Crawler + * @throws ModuleException + */ + protected function getFieldsByLabelOrCss($field) { - $this->assertStringContainsString($uri, $this->_getCurrentUri()); - } + if (is_array($field)) { + $input = $this->strictMatch($field); + if (!count($input)) { + throw new ElementNotFound($field); + } + return $input; + } - public function dontSeeInCurrentUrl($uri) - { - $this->assertStringNotContainsString($uri, $this->_getCurrentUri()); - } + // by label + $label = $this->strictMatch(['xpath' => sprintf('.//label[descendant-or-self::node()[text()[normalize-space()=%s]]]', Crawler::xpathLiteral($field))]); + if (count($label)) { + $label = $label->first(); + if ($label->attr('for')) { + $input = $this->strictMatch(['id' => $label->attr('for')]); + } else { + $input = $this->strictMatch(['xpath' => sprintf('.//label[descendant-or-self::node()[text()[normalize-space()=%s]]]//input', Crawler::xpathLiteral($field))]); + } + } - public function seeCurrentUrlEquals($uri) - { - $this->assertEquals(rtrim($uri, '/'), rtrim($this->_getCurrentUri(), '/')); - } + // by name + if (!isset($input)) { + $input = $this->strictMatch(['name' => $field]); + } - public function dontSeeCurrentUrlEquals($uri) - { - $this->assertNotEquals(rtrim($uri, '/'), rtrim($this->_getCurrentUri(), '/')); - } + // by CSS and XPath + if (!count($input)) { + $input = $this->match($field); + } - public function seeCurrentUrlMatches($uri) - { - $this->assertRegExp($uri, $this->_getCurrentUri()); - } + if (!count($input)) { + throw new ElementNotFound($field, 'Form field by Label or CSS'); + } - public function dontSeeCurrentUrlMatches($uri) - { - $this->assertNotRegExp($uri, $this->_getCurrentUri()); + return $input; } - public function grabFromCurrentUrl($uri = null) + /** + * Returns the DomCrawler\Form object for the form pointed to by + * $node or its closes form parent. + * + * @param Crawler $node + * @return Form + */ + protected function getFormFor(Crawler $node) { - if (!$uri) { - return $this->_getCurrentUri(); - } - $matches = []; - $res = preg_match($uri, $this->_getCurrentUri(), $matches); - if (!$res) { - $this->fail("Couldn't match $uri in " . $this->_getCurrentUri()); + if (strcasecmp($node->first()->getNode(0)->tagName, 'form') === 0) { + $form = $node->first(); + } else { + $form = $node->parents()->filter('form')->first(); } - if (!isset($matches[1])) { - $this->fail("Nothing to grab. A regex parameter required. Ex: '/user/(\\d+)'"); + if (!$form) { + $this->fail('The selected node is not a form and does not have a form ancestor.'); } - return $matches[1]; - } - public function seeCheckboxIsChecked($checkbox) - { - $checkboxes = $this->getFieldsByLabelOrCss($checkbox); - $this->assertDomContains($checkboxes->filter('input[checked=checked]'), 'checkbox'); + $identifier = $form->attr('id') ?: $form->attr('action'); + if (!isset($this->forms[$identifier])) { + $this->forms[$identifier] = $this->getFormFromCrawler($form); + } + return $this->forms[$identifier]; } - public function dontSeeCheckboxIsChecked($checkbox) + /** + * Returns the form action's absolute URL. + * + * @param Crawler $form + * @return string + * @throws TestRuntimeException|ModuleException if either the current + * URL or the URI of the form's action can't be parsed + */ + protected function getFormUrl(Crawler $form) { - $checkboxes = $this->getFieldsByLabelOrCss($checkbox); - $this->assertEquals(0, $checkboxes->filter('input[checked=checked]')->count()); + $action = $form->form()->getUri(); + return $this->getAbsoluteUrlFor($action); } - public function seeInField($field, $value) + /** + * Returns an array of name => value pairs for the passed form. + * + * For form fields containing a name ending in [], an array is + * created out of all field values with the given name. + * + * @param Form $form + * @return array an array of name => value pairs + */ + protected function getFormValuesFor(Form $form) { - $nodes = $this->getFieldsByLabelOrCss($field); - $this->assert($this->proceedSeeInField($nodes, $value)); - } - - public function dontSeeInField($field, $value) - { - $nodes = $this->getFieldsByLabelOrCss($field); - $this->assertNot($this->proceedSeeInField($nodes, $value)); + $values = []; + $fields = $form->all(); + foreach ($fields as $field) { + if ($field instanceof FileFormField || $field->isDisabled() || !$field->hasValue()) { + continue; + } + $fieldName = $this->getSubmissionFormFieldName($field->getName()); + if (substr($field->getName(), -2) === '[]') { + if (!isset($values[$fieldName])) { + $values[$fieldName] = []; + } + $values[$fieldName][] = $field->getValue(); + } else { + $values[$fieldName] = $field->getValue(); + } + } + return $values; } - public function seeInFormFields($formSelector, array $params) + /** + * @param $requestParams + * @return array + */ + protected function getFormPhpValues($requestParams) { - $this->proceedSeeInFormFields($formSelector, $params, false); - } + foreach ($requestParams as $name => $value) { + $qs = http_build_query([$name => $value], '', '&'); + if (!empty($qs)) { + // If the field's name is of the form of "array[key]", + // we'll remove it from the request parameters + // and set the "array" key instead which will contain the actual array. + if (strpos($name, '[') && strpos($name, ']') > strpos($name, '[')) { + unset($requestParams[$name]); + } - public function dontSeeInFormFields($formSelector, array $params) - { - $this->proceedSeeInFormFields($formSelector, $params, true); + parse_str($qs, $expandedValue); + $varName = substr($name, 0, strlen(key($expandedValue))); + $requestParams = array_replace_recursive($requestParams, [$varName => current($expandedValue)]); + } + } + return $requestParams; } - protected function proceedSeeInFormFields($formSelector, array $params, $assertNot) + /** + * Get the values of a set of input fields. + * + * @param Crawler $input + * @return array|string + */ + protected function getInputValue($input) { - $form = $this->match($formSelector)->first(); - if ($form->count() === 0) { - throw new ElementNotFound($formSelector, 'Form'); - } - - $fields = []; - foreach ($params as $name => $values) { - $this->pushFormField($fields, $form, $name, $values); - } + $inputType = $input->attr('type'); + if ($inputType === 'checkbox' || $inputType === 'radio') { + $values = []; - foreach ($fields as list($field, $values)) { - if (!is_array($values)) { - $values = [$values]; + foreach ($input->filter(':checked') as $checkbox) { + $values[] = $checkbox->getAttribute('value'); } - foreach ($values as $value) { - $ret = $this->proceedSeeInField($field, $value); - if ($assertNot) { - $this->assertNot($ret); - } else { - $this->assert($ret); - } - } + return $values; } + + return (new InputFormField($input->getNode(0)))->getValue(); } /** - * Map an array element passed to seeInFormFields to its corresponding field, - * recursing through array values if the field is not found. - * - * @param array $fields The previously found fields. - * @param Crawler $form The form in which to search for fields. - * @param string $name The field's name. - * @param mixed $values - * @return void + * @return string + * @throws ModuleException */ - protected function pushFormField(&$fields, $form, $name, $values) + protected function getNormalizedResponseContent() { - $field = $form->filterXPath(sprintf('.//*[@name=%s]', Crawler::xpathLiteral($name))); + $content = $this->_getResponseContent(); + // Since strip_tags has problems with JS code that contains + // an <= operator the script tags have to be removed manually first. + $content = preg_replace('#(.*?)#is', '', $content); - if ($field->count()) { - $fields[] = [$field, $values]; - } elseif (is_array($values)) { - foreach ($values as $key => $value) { - $this->pushFormField($fields, $form, "{$name}[$key]", $value); - } - } else { - throw new ElementNotFound( - sprintf('//*[@name=%s]', Crawler::xpathLiteral($name)), - 'Form' - ); - } + $content = strip_tags($content); + $content = html_entity_decode($content, ENT_QUOTES); + $content = str_replace("\n", ' ', $content); + $content = preg_replace('/\s{2,}/', ' ', $content); + + return $content; } - protected function proceedSeeInField(Crawler $fields, $value) + /** + * @return int|string + * @throws ModuleException + */ + protected function getResponseStatusCode() { - $testValues = $this->getValueAndTextFromField($fields); - if (!is_array($testValues)) { - $testValues = [$testValues]; + // depending on Symfony version + $response = $this->getRunningClient()->getInternalResponse(); + if (method_exists($response, 'getStatusCode')) { + return $response->getStatusCode(); } - if (is_bool($value) && $value === true && !empty($testValues)) { - $value = reset($testValues); - } elseif (empty($testValues)) { - $testValues = ['']; + if (method_exists($response, 'getStatus')) { + return $response->getStatus(); } - return [ - 'Contains', - (string)$value, - $testValues, - sprintf( - 'Failed asserting that `%s` is in %s\'s value: %s', - $value, - $fields->getNode(0)->nodeName, - var_export($testValues, true) - ) - ]; + return "N/A"; } /** @@ -856,28 +702,6 @@ protected function getValueAndTextFromField(Crawler $nodes) $this->fail("Element $nodes is not a form field or does not contain a form field"); } - /** - * Get the values of a set of input fields. - * - * @param Crawler $input - * @return array|string - */ - protected function getInputValue($input) - { - $inputType = $input->attr('type'); - if ($inputType === 'checkbox' || $inputType === 'radio') { - $values = []; - - foreach ($input->filter(':checked') as $checkbox) { - $values[] = $checkbox->getAttribute('value'); - } - - return $values; - } - - return (new InputFormField($input->getNode(0)))->getValue(); - } - /** * Strips out one pair of trailing square brackets from a field's * name. @@ -893,89 +717,219 @@ protected function getSubmissionFormFieldName($name) return $name; } - /** - * Replaces boolean values in $params with the corresponding field's - * value for checkbox form fields. - * - * The function loops over all input checkbox fields, checking if a - * corresponding key is set in $params. If it is, and the value is - * boolean or an array containing booleans, the value(s) are - * replaced in the array with the real value of the checkbox, and - * the array is returned. - * - * @param Crawler $form the form to find checkbox elements - * @param array $params the parameters to be submitted - * @return array the $params array after replacing bool values - */ - protected function setCheckboxBoolValues(Crawler $form, array $params) + protected function isInternalDomain($domain) { - $checkboxes = $form->filter('input[type=checkbox]'); - $chFoundByName = []; - foreach ($checkboxes as $box) { - $fieldName = $this->getSubmissionFormFieldName($box->getAttribute('name')); - $pos = (!isset($chFoundByName[$fieldName])) ? 0 : $chFoundByName[$fieldName]; - $skip = !isset($params[$fieldName]) - || (!is_array($params[$fieldName]) && !is_bool($params[$fieldName])) - || (is_array($params[$fieldName]) && - ($pos >= count($params[$fieldName]) || !is_bool($params[$fieldName][$pos])) - ); + if ($this->internalDomains === null) { + $this->internalDomains = $this->getInternalDomains(); + } - if ($skip) { - continue; - } - $values = $params[$fieldName]; - if ($values === true) { - $params[$fieldName] = $box->hasAttribute('value') ? $box->getAttribute('value') : 'on'; - $chFoundByName[$fieldName] = $pos + 1; - } elseif (is_array($values)) { - if ($values[$pos] === true) { - $params[$fieldName][$pos] = $box->hasAttribute('value') ? $box->getAttribute('value') : 'on'; - $chFoundByName[$fieldName] = $pos + 1; - } else { - array_splice($params[$fieldName], $pos, 1); - } - } else { - unset($params[$fieldName]); + foreach ($this->internalDomains as $pattern) { + if (preg_match($pattern, $domain)) { + return true; } } - return $params; + return false; } /** - * Submits the form currently selected in the passed Crawler, after - * setting any values passed in $params and setting the value of the - * passed button name. + * @param $selector * - * @param Crawler $frmCrawl the form to submit - * @param array $params additional parameter values to set on the - * form - * @param string $button the name of a submit button in the form + * @return Crawler + * @throws ModuleException */ - protected function proceedSubmitForm(Crawler $frmCrawl, array $params, $button = null) + protected function match($selector) { - $url = null; - $form = $this->getFormFor($frmCrawl); - $defaults = $this->getFormValuesFor($form); - $merged = array_merge($defaults, $params); - $requestParams = $this->setCheckboxBoolValues($frmCrawl, $merged); - - if (!empty($button)) { - $btnCrawl = $frmCrawl->filterXPath(sprintf( - '//*[not(@disabled) and @type="submit" and @name=%s]', - Crawler::xpathLiteral($button) - )); - if (count($btnCrawl)) { - $requestParams[$button] = $btnCrawl->attr('value'); - $formaction = $btnCrawl->attr('formaction'); - if ($formaction) { - $url = $formaction; - } - } + if (is_array($selector)) { + return $this->strictMatch($selector); } - if (!$url) { - $url = $this->getFormUrl($frmCrawl); - } + if (Locator::isCSS($selector)) { + return $this->getCrawler()->filter($selector); + } + if (Locator::isXPath($selector)) { + return $this->getCrawler()->filterXPath($selector); + } + throw new MalformedLocatorException($selector, 'XPath or CSS'); + } + + /** + * @param $name + * @param $form + * @param $dynamicField + * @return FormField + */ + protected function matchFormField($name, $form, $dynamicField) + { + if (substr($name, -2) !== '[]') { + return $form[$name]; + } + $name = substr($name, 0, -2); + /** @var $item FormField */ + foreach ($form[$name] as $item) { + if ($item == $dynamicField) { + return $item; + } + } + throw new TestRuntimeException("None of form fields by {$name}[] were not matched"); + } + + protected function matchOption(Crawler $field, $option) + { + if (isset($option['value'])) { + return $option['value']; + } + if (isset($option['text'])) { + $option = $option['text']; + } + $options = $field->filterXPath(sprintf('//option[text()=normalize-space("%s")]|//input[@type="radio" and @value=normalize-space("%s")]', $option, $option)); + if ($options->count()) { + $firstMatchingDomNode = $options->getNode(0); + if ($firstMatchingDomNode->tagName === 'option') { + $firstMatchingDomNode->setAttribute('selected', 'selected'); + } else { + $firstMatchingDomNode->setAttribute('checked', 'checked'); + } + $valueAttribute = $options->first()->attr('value'); + //attr() returns null when option has no value attribute + if ($valueAttribute !== null) { + return $valueAttribute; + } + return $options->first()->text(); + } + return $option; + } + + /** + * @param $select + * @return object|Crawler + * @throws ModuleException + */ + protected function matchSelectedOption($select) + { + $nodes = $this->getFieldsByLabelOrCss($select); + $selectedOptions = $nodes->filter('option[selected],input:checked'); + if ($selectedOptions->count() === 0) { + $selectedOptions = $nodes->filter('option,input')->first(); + } + return $selectedOptions; + } + + /** + * @param $option + * @return ChoiceFormField + * @throws ModuleException + */ + protected function proceedCheckOption($option) + { + $form = $this->getFormFor($field = $this->getFieldByLabelOrCss($option)); + $name = $field->attr('name'); + + if ($field->getNode(0) === null) { + throw new TestRuntimeException("Form field $name is not located"); + } + // If the name is an array than we compare objects to find right checkbox + $formField = $this->matchFormField($name, $form, new ChoiceFormField($field->getNode(0))); + $field->getNode(0)->setAttribute('checked', 'checked'); + if (!$formField instanceof ChoiceFormField) { + throw new TestRuntimeException("Form field $name is not a checkable"); + } + return $formField; + } + + protected function proceedSeeInField(Crawler $fields, $value) + { + $testValues = $this->getValueAndTextFromField($fields); + if (!is_array($testValues)) { + $testValues = [$testValues]; + } + if (is_bool($value) && $value === true && !empty($testValues)) { + $value = reset($testValues); + } elseif (empty($testValues)) { + $testValues = ['']; + } + return [ + 'Contains', + (string)$value, + $testValues, + sprintf( + 'Failed asserting that `%s` is in %s\'s value: %s', + $value, + $fields->getNode(0)->nodeName, + var_export($testValues, true) + ) + ]; + } + + /** + * @param $formSelector + * @param array $params + * @param $assertNot + * @throws ModuleException + */ + protected function proceedSeeInFormFields($formSelector, array $params, $assertNot) + { + $form = $this->match($formSelector)->first(); + if ($form->count() === 0) { + throw new ElementNotFound($formSelector, 'Form'); + } + + $fields = []; + foreach ($params as $name => $values) { + $this->pushFormField($fields, $form, $name, $values); + } + + foreach ($fields as list($field, $values)) { + if (!is_array($values)) { + $values = [$values]; + } + + foreach ($values as $value) { + $ret = $this->proceedSeeInField($field, $value); + if ($assertNot) { + $this->assertNot($ret); + } else { + $this->assert($ret); + } + } + } + } + + /** + * Submits the form currently selected in the passed Crawler, after + * setting any values passed in $params and setting the value of the + * passed button name. + * + * @param Crawler $frmCrawl the form to submit + * @param array $params additional parameter values to set on the + * form + * @param string $button the name of a submit button in the form + * @throws ExternalUrlException|ModuleException + */ + protected function proceedSubmitForm(Crawler $frmCrawl, array $params, $button = null) + { + $url = null; + $form = $this->getFormFor($frmCrawl); + $defaults = $this->getFormValuesFor($form); + $merged = array_merge($defaults, $params); + $requestParams = $this->setCheckboxBoolValues($frmCrawl, $merged); + + if (!empty($button)) { + $btnCrawl = $frmCrawl->filterXPath(sprintf( + '//*[not(@disabled) and @type="submit" and @name=%s]', + Crawler::xpathLiteral($button) + )); + if (count($btnCrawl)) { + $requestParams[$button] = $btnCrawl->attr('value'); + $formaction = $btnCrawl->attr('formaction'); + if ($formaction) { + $url = $formaction; + } + } + } + + if (!$url) { + $url = $this->getFormUrl($frmCrawl); + } if (strcasecmp($form->getMethod(), 'GET') === 0) { $url = Uri::mergeUrls($url, '?' . http_build_query($requestParams)); @@ -998,100 +952,601 @@ protected function proceedSubmitForm(Crawler $frmCrawl, array $params, $button = $this->forms = []; } - public function submitForm($selector, array $params, $button = null) + /** + * Map an array element passed to seeInFormFields to its corresponding field, + * recursing through array values if the field is not found. + * + * @param array $fields The previously found fields. + * @param Crawler $form The form in which to search for fields. + * @param string $name The field's name. + * @param mixed $values + * @return void + */ + protected function pushFormField(&$fields, $form, $name, $values) { - $form = $this->match($selector)->first(); - if (!count($form)) { - throw new ElementNotFound($this->stringifySelector($selector), 'Form'); + $field = $form->filterXPath(sprintf('.//*[@name=%s]', Crawler::xpathLiteral($name))); + + if ($field->count()) { + $fields[] = [$field, $values]; + } elseif (is_array($values)) { + foreach ($values as $key => $value) { + $this->pushFormField($fields, $form, "{$name}[$key]", $value); + } + } else { + throw new ElementNotFound( + sprintf('//*[@name=%s]', Crawler::xpathLiteral($name)), + 'Form' + ); } - $this->proceedSubmitForm($form, $params, $button); } /** - * Returns an absolute URL for the passed URI with the current URL - * as the base path. - * - * @param string $uri the absolute or relative URI - * @return string the absolute URL - * @throws \Codeception\Exception\TestRuntimeException if either the current - * URL or the passed URI can't be parsed + * @param $result + * @param $maxRedirects + * @param $redirectCount + * @return mixed + * @throws ModuleException */ - protected function getAbsoluteUrlFor($uri) + protected function redirectIfNecessary($result, $maxRedirects, $redirectCount) { - $currentUrl = $this->getRunningClient()->getHistory()->current()->getUri(); - if (empty($uri) || strpos($uri, '#') === 0) { - return $currentUrl; + $locationHeader = $this->client->getInternalResponse()->getHeader('Location'); + $statusCode = $this->getResponseStatusCode(); + if ($locationHeader && $statusCode >= 300 && $statusCode < 400) { + if ($redirectCount === $maxRedirects) { + throw new LogicException(sprintf( + 'The maximum number (%d) of redirections was reached.', + $maxRedirects + )); + } + + $this->debugSection('Redirecting to', $locationHeader); + + $result = $this->client->followRedirect(); + $this->debugResponse($locationHeader); + return $this->redirectIfNecessary($result, $maxRedirects, $redirectCount + 1); } - return Uri::mergeUrls($currentUrl, $uri); + $this->client->followRedirects(true); + return $result; } /** - * Returns the form action's absolute URL. + * Replaces boolean values in $params with the corresponding field's + * value for checkbox form fields. * - * @param \Symfony\Component\DomCrawler\Crawler $form - * @return string - * @throws \Codeception\Exception\TestRuntimeException if either the current - * URL or the URI of the form's action can't be parsed - */ - protected function getFormUrl(Crawler $form) - { - $action = $form->form()->getUri(); - return $this->getAbsoluteUrlFor($action); + * The function loops over all input checkbox fields, checking if a + * corresponding key is set in $params. If it is, and the value is + * boolean or an array containing booleans, the value(s) are + * replaced in the array with the real value of the checkbox, and + * the array is returned. + * + * @param Crawler $form the form to find checkbox elements + * @param array $params the parameters to be submitted + * @return array the $params array after replacing bool values + */ + protected function setCheckboxBoolValues(Crawler $form, array $params) + { + $checkboxes = $form->filter('input[type=checkbox]'); + $chFoundByName = []; + foreach ($checkboxes as $box) { + $fieldName = $this->getSubmissionFormFieldName($box->getAttribute('name')); + $pos = (!isset($chFoundByName[$fieldName])) ? 0 : $chFoundByName[$fieldName]; + $skip = !isset($params[$fieldName]) + || (!is_array($params[$fieldName]) && !is_bool($params[$fieldName])) + || (is_array($params[$fieldName]) && + ($pos >= count($params[$fieldName]) || !is_bool($params[$fieldName][$pos])) + ); + + if ($skip) { + continue; + } + $values = $params[$fieldName]; + if ($values === true) { + $params[$fieldName] = $box->hasAttribute('value') ? $box->getAttribute('value') : 'on'; + $chFoundByName[$fieldName] = $pos + 1; + } elseif (is_array($values)) { + if ($values[$pos] === true) { + $params[$fieldName][$pos] = $box->hasAttribute('value') ? $box->getAttribute('value') : 'on'; + $chFoundByName[$fieldName] = $pos + 1; + } else { + array_splice($params[$fieldName], $pos, 1); + } + } else { + unset($params[$fieldName]); + } + } + return $params; + } + + /** + * @param array $by + * @return Crawler + *@throws TestRuntimeException|ModuleException + */ + protected function strictMatch(array $by) + { + $type = key($by); + $locator = $by[$type]; + switch ($type) { + case 'id': + return $this->filterByCSS("#$locator"); + case 'name': + return $this->filterByXPath(sprintf('.//*[@name=%s]', Crawler::xpathLiteral($locator))); + case 'css': + return $this->filterByCSS($locator); + case 'xpath': + return $this->filterByXPath($locator); + case 'link': + return $this->filterByXPath(sprintf('.//a[.=%s or contains(./@title, %s)]', Crawler::xpathLiteral($locator), Crawler::xpathLiteral($locator))); + case 'class': + return $this->filterByCSS(".$locator"); + default: + throw new TestRuntimeException( + "Locator type '$by' is not defined. Use either: xpath, css, id, link, class, name" + ); + } + } + + protected function setCookiesFromOptions() + { + if (isset($this->config['cookies']) && is_array($this->config['cookies']) && !empty($this->config['cookies'])) { + $domain = parse_url($this->config['url'], PHP_URL_HOST); + $cookieJar = $this->client->getCookieJar(); + foreach ($this->config['cookies'] as &$cookie) { + if (!is_array($cookie) || !array_key_exists('Name', $cookie) || !array_key_exists('Value', $cookie)) { + throw new InvalidArgumentException('Cookies must have at least Name and Value attributes'); + } + if (!isset($cookie['Domain'])) { + $cookie['Domain'] = $domain; + } + if (!isset($cookie['Expires'])) { + $cookie['Expires'] = null; + } + if (!isset($cookie['Path'])) { + $cookie['Path'] = '/'; + } + if (!isset($cookie['Secure'])) { + $cookie['Secure'] = false; + } + if (!isset($cookie['HttpOnly'])) { + $cookie['HttpOnly'] = false; + } + $cookieJar->set(new Cookie( + $cookie['Name'], + $cookie['Value'], + $cookie['Expires'], + $cookie['Path'], + $cookie['Domain'], + $cookie['Secure'], + $cookie['HttpOnly'] + )); + } + } + } + + public function _failed(TestInterface $test, $fail) + { + try { + if (!$this->client || !$this->client->getInternalResponse()) { + return; + } + } catch (BadMethodCallException $e) { + //Symfony 5 throws exception if request() method threw an exception. + //The "request()" method must be called before "Symfony\Component\BrowserKit\AbstractBrowser::getInternalResponse()" + return; + } + $filename = preg_replace('~\W~', '.', Descriptor::getTestSignatureUnique($test)); + + $extensions = [ + 'application/json' => 'json', + 'text/xml' => 'xml', + 'application/xml' => 'xml', + 'text/plain' => 'txt' + ]; + + try { + $internalResponse = $this->client->getInternalResponse(); + } catch (BadMethodCallException $e) { + $internalResponse = false; + } + + $responseContentType = $internalResponse ? $internalResponse->getHeader('content-type') : ''; + list($responseMimeType) = explode(';', $responseContentType); + + $extension = isset($extensions[$responseMimeType]) ? $extensions[$responseMimeType] : 'html'; + + $filename = mb_strcut($filename, 0, 244, 'utf-8') . '.fail.' . $extension; + $this->_savePageSource($report = codecept_output_dir() . $filename); + $test->getMetadata()->addReport('html', $report); + $test->getMetadata()->addReport('response', $report); + } + + public function _after(TestInterface $test) + { + $this->client = null; + $this->crawler = null; + $this->forms = []; + $this->headers = []; + } + + public function _conflicts() + { + return 'Codeception\Lib\Interfaces\Web'; + } + + public function _findElements($locator) + { + return $this->match($locator); + } + + /** + * Send custom request to a backend using method, uri, parameters, etc. + * Use it in Helpers to create special request actions, like accessing API + * Returns a string with response body. + * + * ```php + * getModule('{{MODULE_NAME}}')->_request('POST', '/api/v1/users', ['name' => $name]); + * $user = json_decode($userData); + * return $user->id; + * } + * ?> + * ``` + * Does not load the response into the module so you can't interact with response page (click, fill forms). + * To load arbitrary page for interaction, use `_loadPage` method. + * + * @api + * @param $method + * @param $uri + * @param array $parameters + * @param array $files + * @param array $server + * @param null $content + * @return mixed|Crawler + * @throws ExternalUrlException + * @see `_loadPage` + */ + public function _request( + $method, + $uri, + array $parameters = [], + array $files = [], + array $server = [], + $content = null + ) { + $this->clientRequest($method, $uri, $parameters, $files, $server, $content, true); + return $this->_getResponseContent(); + } + + /** + * Returns content of the last response + * Use it in Helpers when you want to retrieve response of request performed by another module. + * + * ```php + * assertStringContainsString($text, $this->getModule('{{MODULE_NAME}}')->_getResponseContent(), "response contains"); + * } + * ?> + * ``` + * + * @api + * @return string + * @throws ModuleException + */ + public function _getResponseContent() + { + return (string)$this->getRunningClient()->getInternalResponse()->getContent(); + } + + /** + * Opens a page with arbitrary request parameters. + * Useful for testing multi-step forms on a specific step. + * + * ```php + * getModule('{{MODULE_NAME}}')->_loadPage('POST', '/checkout/step2', ['order' => $orderId]); + * } + * ?> + * ``` + * + * @api + * @param $method + * @param $uri + * @param array $parameters + * @param array $files + * @param array $server + * @param null $content + */ + public function _loadPage( + $method, + $uri, + array $parameters = [], + array $files = [], + array $server = [], + $content = null + ) { + $this->crawler = $this->clientRequest($method, $uri, $parameters, $files, $server, $content); + $this->baseUrl = $this->retrieveBaseUrl(); + $this->forms = []; + } + + public function _savePageSource($filename) + { + file_put_contents($filename, $this->_getResponseContent()); + } + + /** + * Authenticates user for HTTP_AUTH + * + * @param $username + * @param $password + */ + public function amHttpAuthenticated($username, $password) + { + $this->client->setServerParameter('PHP_AUTH_USER', $username); + $this->client->setServerParameter('PHP_AUTH_PW', $password); + } + + /** + * Sets the HTTP header to the passed value - which is used on + * subsequent HTTP requests through PhpBrowser. + * + * Example: + * ```php + * haveHttpHeader('X-Requested-With', 'Codeception'); + * $I->amOnPage('test-headers.php'); + * ?> + * ``` + * + * To use special chars in Header Key use HTML Character Entities: + * Example: + * Header with underscore - 'Client_Id' + * should be represented as - 'Client_Id' or 'Client_Id' + * + * ```php + * haveHttpHeader('Client_Id', 'Codeception'); + * ?> + * ``` + * + * @param string $name the name of the request header + * @param string $value the value to set it to for subsequent + * requests + */ + public function haveHttpHeader($name, $value) + { + $name = implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $name))))); + $this->headers[$name] = $value; + } + + /** + * Deletes the header with the passed name. Subsequent requests + * will not have the deleted header in its request. + * + * Example: + * ```php + * haveHttpHeader('X-Requested-With', 'Codeception'); + * $I->amOnPage('test-headers.php'); + * // ... + * $I->deleteHeader('X-Requested-With'); + * $I->amOnPage('some-other-page.php'); + * ?> + * ``` + * + * @param string $name the name of the header to delete. + */ + public function deleteHeader($name) + { + $name = implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $name))))); + unset($this->headers[$name]); + } + + + public function amOnPage($page) + { + $this->_loadPage('GET', $page); + } + + public function click($link, $context = null) + { + if ($context) { + $this->crawler = $this->match($context); + } + + if (is_array($link)) { + $this->clickByLocator($link); + return; + } + + $anchor = $this->strictMatch(['link' => $link]); + if (!count($anchor)) { + $anchor = $this->getCrawler()->selectLink($link); + } + if (count($anchor)) { + $this->openHrefFromDomNode($anchor->getNode(0)); + return; + } + + $buttonText = str_replace('"', "'", $link); + $button = $this->crawler->selectButton($buttonText); + + if (count($button) && $this->clickButton($button->getNode(0))) { + return; + } + + try { + $this->clickByLocator($link); + } catch (MalformedLocatorException $e) { + throw new ElementNotFound("name=$link", "'$link' is invalid CSS and XPath selector and Link or Button"); + } + } + + public function see($text, $selector = null) + { + if (!$selector) { + $this->assertPageContains($text); + return; + } + + $nodes = $this->match($selector); + $this->assertDomContains($nodes, $this->stringifySelector($selector), $text); + } + + public function dontSee($text, $selector = null) + { + if (!$selector) { + $this->assertPageNotContains($text); + return; + } + + $nodes = $this->match($selector); + $this->assertDomNotContains($nodes, $this->stringifySelector($selector), $text); + } + + public function seeInSource($raw) + { + $this->assertPageSourceContains($raw); + } + + public function dontSeeInSource($raw) + { + $this->assertPageSourceNotContains($raw); + } + + public function seeLink($text, $url = null) + { + $crawler = $this->getCrawler()->selectLink($text); + if ($crawler->count() === 0) { + $this->fail("No links containing text '$text' were found in page " . $this->_getCurrentUri()); + } + if ($url) { + $crawler = $crawler->filterXPath(sprintf('.//a[substring(@href, string-length(@href) - string-length(%1$s) + 1)=%1$s]', Crawler::xpathLiteral($url))); + if ($crawler->count() === 0) { + $this->fail("No links containing text '$text' and URL '$url' were found in page " . $this->_getCurrentUri()); + } + } + $this->assertTrue(true); + } + + public function dontSeeLink($text, $url = '') + { + $crawler = $this->getCrawler()->selectLink($text); + if (!$url && $crawler->count() > 0) { + $this->fail("Link containing text '$text' was found in page " . $this->_getCurrentUri()); + } + $crawler = $crawler->filterXPath( + sprintf('.//a[substring(@href, string-length(@href) - string-length(%1$s) + 1)=%1$s]', + Crawler::xpathLiteral($url)) + ); + if ($crawler->count() > 0) { + $this->fail("Link containing text '$text' and URL '$url' was found in page " . $this->_getCurrentUri()); + } + } + + /** + * @return string + * @throws ModuleException + */ + public function _getCurrentUri() + { + return Uri::retrieveUri($this->getRunningClient()->getHistory()->current()->getUri()); + } + + public function seeInCurrentUrl($uri) + { + $this->assertStringContainsString($uri, $this->_getCurrentUri()); + } + + public function dontSeeInCurrentUrl($uri) + { + $this->assertStringNotContainsString($uri, $this->_getCurrentUri()); + } + + public function seeCurrentUrlEquals($uri) + { + $this->assertEquals(rtrim($uri, '/'), rtrim($this->_getCurrentUri(), '/')); + } + + public function dontSeeCurrentUrlEquals($uri) + { + $this->assertNotEquals(rtrim($uri, '/'), rtrim($this->_getCurrentUri(), '/')); + } + + public function seeCurrentUrlMatches($uri) + { + $this->assertRegExp($uri, $this->_getCurrentUri()); + } + + public function dontSeeCurrentUrlMatches($uri) + { + $this->assertNotRegExp($uri, $this->_getCurrentUri()); + } + + public function grabFromCurrentUrl($uri = null) + { + if (!$uri) { + return $this->_getCurrentUri(); + } + $matches = []; + $res = preg_match($uri, $this->_getCurrentUri(), $matches); + if (!$res) { + $this->fail("Couldn't match $uri in " . $this->_getCurrentUri()); + } + if (!isset($matches[1])) { + $this->fail("Nothing to grab. A regex parameter required. Ex: '/user/(\\d+)'"); + } + return $matches[1]; + } + + public function seeCheckboxIsChecked($checkbox) + { + $checkboxes = $this->getFieldsByLabelOrCss($checkbox); + $this->assertDomContains($checkboxes->filter('input[checked=checked]'), 'checkbox'); + } + + public function dontSeeCheckboxIsChecked($checkbox) + { + $checkboxes = $this->getFieldsByLabelOrCss($checkbox); + $this->assertEquals(0, $checkboxes->filter('input[checked=checked]')->count()); + } + + public function seeInField($field, $value) + { + $nodes = $this->getFieldsByLabelOrCss($field); + $this->assert($this->proceedSeeInField($nodes, $value)); + } + + public function dontSeeInField($field, $value) + { + $nodes = $this->getFieldsByLabelOrCss($field); + $this->assertNot($this->proceedSeeInField($nodes, $value)); + } + + public function seeInFormFields($formSelector, array $params) + { + $this->proceedSeeInFormFields($formSelector, $params, false); } - /** - * Returns the DomCrawler\Form object for the form pointed to by - * $node or its closes form parent. - * - * @param \Symfony\Component\DomCrawler\Crawler $node - * @return \Symfony\Component\DomCrawler\Form - */ - protected function getFormFor(Crawler $node) + public function dontSeeInFormFields($formSelector, array $params) { - if (strcasecmp($node->first()->getNode(0)->tagName, 'form') === 0) { - $form = $node->first(); - } else { - $form = $node->parents()->filter('form')->first(); - } - if (!$form) { - $this->fail('The selected node is not a form and does not have a form ancestor.'); - } - - $identifier = $form->attr('id') ?: $form->attr('action'); - if (!isset($this->forms[$identifier])) { - $this->forms[$identifier] = $this->getFormFromCrawler($form); - } - return $this->forms[$identifier]; + $this->proceedSeeInFormFields($formSelector, $params, true); } - /** - * Returns an array of name => value pairs for the passed form. - * - * For form fields containing a name ending in [], an array is - * created out of all field values with the given name. - * - * @param \Symfony\Component\DomCrawler\Form the form - * @return array an array of name => value pairs - */ - protected function getFormValuesFor(Form $form) + public function submitForm($selector, array $params, $button = null) { - $values = []; - $fields = $form->all(); - foreach ($fields as $field) { - if ($field instanceof FileFormField || $field->isDisabled() || !$field->hasValue()) { - continue; - } - $fieldName = $this->getSubmissionFormFieldName($field->getName()); - if (substr($field->getName(), -2) === '[]') { - if (!isset($values[$fieldName])) { - $values[$fieldName] = []; - } - $values[$fieldName][] = $field->getValue(); - } else { - $values[$fieldName] = $field->getValue(); - } + $form = $this->match($selector)->first(); + if (!count($form)) { + throw new ElementNotFound($this->stringifySelector($selector), 'Form'); } - return $values; + $this->proceedSubmitForm($form, $params, $button); } public function fillField($field, $value) @@ -1111,55 +1566,6 @@ public function fillField($field, $value) } } - /** - * @param $field - * - * @return \Symfony\Component\DomCrawler\Crawler - */ - protected function getFieldsByLabelOrCss($field) - { - if (is_array($field)) { - $input = $this->strictMatch($field); - if (!count($input)) { - throw new ElementNotFound($field); - } - return $input; - } - - // by label - $label = $this->strictMatch(['xpath' => sprintf('.//label[descendant-or-self::node()[text()[normalize-space()=%s]]]', Crawler::xpathLiteral($field))]); - if (count($label)) { - $label = $label->first(); - if ($label->attr('for')) { - $input = $this->strictMatch(['id' => $label->attr('for')]); - } else { - $input = $this->strictMatch(['xpath' => sprintf('.//label[descendant-or-self::node()[text()[normalize-space()=%s]]]//input', Crawler::xpathLiteral($field))]); - } - } - - // by name - if (!isset($input)) { - $input = $this->strictMatch(['name' => $field]); - } - - // by CSS and XPath - if (!count($input)) { - $input = $this->match($field); - } - - if (!count($input)) { - throw new ElementNotFound($field, 'Form field by Label or CSS'); - } - - return $input; - } - - protected function getFieldByLabelOrCss($field) - { - $input = $this->getFieldsByLabelOrCss($field); - return $input->first(); - } - public function selectOption($select, $option) { $field = $this->getFieldByLabelOrCss($select); @@ -1200,32 +1606,6 @@ public function selectOption($select, $option) $formField->select($this->matchOption($field, $option)); } - protected function matchOption(Crawler $field, $option) - { - if (isset($option['value'])) { - return $option['value']; - } - if (isset($option['text'])) { - $option = $option['text']; - } - $options = $field->filterXPath(sprintf('//option[text()=normalize-space("%s")]|//input[@type="radio" and @value=normalize-space("%s")]', $option, $option)); - if ($options->count()) { - $firstMatchingDomNode = $options->getNode(0); - if ($firstMatchingDomNode->tagName === 'option') { - $firstMatchingDomNode->setAttribute('selected', 'selected'); - } else { - $firstMatchingDomNode->setAttribute('checked', 'checked'); - } - $valueAttribute = $options->first()->attr('value'); - //attr() returns null when option has no value attribute - if ($valueAttribute !== null) { - return $valueAttribute; - } - return $options->first()->text(); - } - return $option; - } - public function checkOption($option) { $this->proceedCheckOption($option)->tick(); @@ -1236,36 +1616,15 @@ public function uncheckOption($option) $this->proceedCheckOption($option)->untick(); } - /** - * @param $option - * @return ChoiceFormField - */ - protected function proceedCheckOption($option) - { - $form = $this->getFormFor($field = $this->getFieldByLabelOrCss($option)); - $name = $field->attr('name'); - - if ($field->getNode(0) === null) { - throw new TestRuntimeException("Form field $name is not located"); - } - // If the name is an array than we compare objects to find right checkbox - $formField = $this->matchFormField($name, $form, new ChoiceFormField($field->getNode(0))); - $field->getNode(0)->setAttribute('checked', 'checked'); - if (!$formField instanceof ChoiceFormField) { - throw new TestRuntimeException("Form field $name is not a checkable"); - } - return $formField; - } - public function attachFile($field, $filename) { $form = $this->getFormFor($field = $this->getFieldByLabelOrCss($field)); $filePath = codecept_data_dir() . $filename; if (!file_exists($filePath)) { - throw new \InvalidArgumentException("File does not exist: $filePath"); + throw new InvalidArgumentException("File does not exist: $filePath"); } if (!is_readable($filePath)) { - throw new \InvalidArgumentException("File is not readable: $filePath"); + throw new InvalidArgumentException("File is not readable: $filePath"); } $name = $field->attr('name'); @@ -1318,126 +1677,41 @@ public function sendAjaxPostRequest($uri, $params = []) } /** - * Sends an ajax request, using the passed HTTP method. - * See `sendAjaxPostRequest()` - * Example: - * ``` php - * sendAjaxRequest('PUT', '/posts/7', ['title' => 'new title']); - * ``` - * - * @param $method - * @param $uri - * @param $params - */ - public function sendAjaxRequest($method, $uri, $params = []) - { - $this->clientRequest($method, $uri, $params, [], ['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'], null, false); - } - - /** - * @param $url - */ - protected function debugResponse($url) - { - $this->debugSection('Page', $url); - $this->debugSection('Response', $this->getResponseStatusCode()); - $this->debugSection('Request Cookies', $this->getRunningClient()->getInternalRequest()->getCookies()); - $this->debugSection('Response Headers', $this->getRunningClient()->getInternalResponse()->getHeaders()); - } - - public function makeHtmlSnapshot($name = null) - { - if (empty($name)) { - $name = uniqid(date("Y-m-d_H-i-s_"), true); - } - $debugDir = codecept_output_dir() . 'debug'; - if (!is_dir($debugDir)) { - mkdir($debugDir, 0777); - } - $fileName = $debugDir . DIRECTORY_SEPARATOR . $name . '.html'; - - $this->_savePageSource($fileName); - $this->debugSection('Snapshot Saved', "file://$fileName"); - } - - public function _getResponseStatusCode() - { - return $this->getResponseStatusCode(); - } - - protected function getResponseStatusCode() - { - // depending on Symfony version - $response = $this->getRunningClient()->getInternalResponse(); - if (method_exists($response, 'getStatusCode')) { - return $response->getStatusCode(); - } - if (method_exists($response, 'getStatus')) { - return $response->getStatus(); - } - return "N/A"; - } - - /** - * @param $selector - * - * @return Crawler - */ - protected function match($selector) - { - if (is_array($selector)) { - return $this->strictMatch($selector); - } - - if (Locator::isCSS($selector)) { - return $this->getCrawler()->filter($selector); - } - if (Locator::isXPath($selector)) { - return $this->getCrawler()->filterXPath($selector); - } - throw new MalformedLocatorException($selector, 'XPath or CSS'); - } - - /** - * @param array $by - * @throws TestRuntimeException - * @return Crawler - */ - protected function strictMatch(array $by) - { - $type = key($by); - $locator = $by[$type]; - switch ($type) { - case 'id': - return $this->filterByCSS("#$locator"); - case 'name': - return $this->filterByXPath(sprintf('.//*[@name=%s]', Crawler::xpathLiteral($locator))); - case 'css': - return $this->filterByCSS($locator); - case 'xpath': - return $this->filterByXPath($locator); - case 'link': - return $this->filterByXPath(sprintf('.//a[.=%s or contains(./@title, %s)]', Crawler::xpathLiteral($locator), Crawler::xpathLiteral($locator))); - case 'class': - return $this->filterByCSS(".$locator"); - default: - throw new TestRuntimeException( - "Locator type '$by' is not defined. Use either: xpath, css, id, link, class, name" - ); - } + * Sends an ajax request, using the passed HTTP method. + * See `sendAjaxPostRequest()` + * Example: + * ``` php + * sendAjaxRequest('PUT', '/posts/7', ['title' => 'new title']); + * ``` + * + * @param $method + * @param $uri + * @param $params + */ + public function sendAjaxRequest($method, $uri, $params = []) + { + $this->clientRequest($method, $uri, $params, [], ['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'], null, false); } - protected function filterByAttributes(Crawler $nodes, array $attributes) + public function makeHtmlSnapshot($name = null) { - foreach ($attributes as $attr => $val) { - $nodes = $nodes->reduce( - static function (Crawler $node) use ($attr, $val) { - return $node->attr($attr) === $val; - } - ); + if (empty($name)) { + $name = uniqid(date("Y-m-d_H-i-s_"), true); } - return $nodes; + $debugDir = codecept_output_dir() . 'debug'; + if (!is_dir($debugDir)) { + mkdir($debugDir, 0777); + } + $fileName = $debugDir . DIRECTORY_SEPARATOR . $name . '.html'; + + $this->_savePageSource($fileName); + $this->debugSection('Snapshot Saved', "file://$fileName"); + } + + public function _getResponseStatusCode() + { + return $this->getResponseStatusCode(); } public function grabTextFrom($cssOrXPathOrRegex) @@ -1644,16 +1918,6 @@ public function dontSeeOptionIsSelected($selector, $optionText) $this->assertNotEquals($optionText, $value); } - protected function matchSelectedOption($select) - { - $nodes = $this->getFieldsByLabelOrCss($select); - $selectedOptions = $nodes->filter('option[selected],input:checked'); - if ($selectedOptions->count() === 0) { - $selectedOptions = $nodes->filter('option,input')->first(); - } - return $selectedOptions; - } - /** * Asserts that current page has 404 response status code. */ @@ -1775,155 +2039,6 @@ public function dontSeeInTitle($title) $this->assertStringNotContainsString($title, $nodes->first()->text(), "page title contains $title"); } - protected function assertDomContains($nodes, $message, $text = '') - { - $constraint = new CrawlerConstraint($text, $this->_getCurrentUri()); - $this->assertThat($nodes, $constraint, $message); - } - - protected function assertDomNotContains($nodes, $message, $text = '') - { - $constraint = new CrawlerNotConstraint($text, $this->_getCurrentUri()); - $this->assertThat($nodes, $constraint, $message); - } - - protected function assertPageContains($needle, $message = '') - { - $constraint = new PageConstraint($needle, $this->_getCurrentUri()); - $this->assertThat( - $this->getNormalizedResponseContent(), - $constraint, - $message - ); - } - - protected function assertPageNotContains($needle, $message = '') - { - $constraint = new PageConstraint($needle, $this->_getCurrentUri()); - $this->assertThatItsNot( - $this->getNormalizedResponseContent(), - $constraint, - $message - ); - } - - protected function assertPageSourceContains($needle, $message = '') - { - $constraint = new PageConstraint($needle, $this->_getCurrentUri()); - $this->assertThat( - $this->_getResponseContent(), - $constraint, - $message - ); - } - - protected function assertPageSourceNotContains($needle, $message = '') - { - $constraint = new PageConstraint($needle, $this->_getCurrentUri()); - $this->assertThatItsNot( - $this->_getResponseContent(), - $constraint, - $message - ); - } - - /** - * @param $name - * @param $form - * @param $dynamicField - * @return FormField - */ - protected function matchFormField($name, $form, $dynamicField) - { - if (substr($name, -2) !== '[]') { - return $form[$name]; - } - $name = substr($name, 0, -2); - /** @var $item \Symfony\Component\DomCrawler\Field\FormField */ - foreach ($form[$name] as $item) { - if ($item == $dynamicField) { - return $item; - } - } - throw new TestRuntimeException("None of form fields by {$name}[] were not matched"); - } - - /** - * @param $locator - * @return Crawler - */ - protected function filterByCSS($locator) - { - if (!Locator::isCSS($locator)) { - throw new MalformedLocatorException($locator, 'css'); - } - return $this->getCrawler()->filter($locator); - } - - /** - * @param $locator - * @return Crawler - */ - protected function filterByXPath($locator) - { - if (!Locator::isXPath($locator)) { - throw new MalformedLocatorException($locator, 'xpath'); - } - return $this->getCrawler()->filterXPath($locator); - } - - /** - * @param $requestParams - * @return array - */ - protected function getFormPhpValues($requestParams) - { - foreach ($requestParams as $name => $value) { - $qs = http_build_query([$name => $value], '', '&'); - if (!empty($qs)) { - // If the field's name is of the form of "array[key]", - // we'll remove it from the request parameters - // and set the "array" key instead which will contain the actual array. - if (strpos($name, '[') && strpos($name, ']') > strpos($name, '[')) { - unset($requestParams[$name]); - } - - parse_str($qs, $expandedValue); - $varName = substr($name, 0, strlen(key($expandedValue))); - $requestParams = array_replace_recursive($requestParams, [$varName => current($expandedValue)]); - } - } - return $requestParams; - } - - /** - * @param $result - * @param $maxRedirects - * @param $redirectCount - * @return mixed - */ - protected function redirectIfNecessary($result, $maxRedirects, $redirectCount) - { - $locationHeader = $this->client->getInternalResponse()->getHeader('Location'); - $statusCode = $this->getResponseStatusCode(); - if ($locationHeader && $statusCode >= 300 && $statusCode < 400) { - if ($redirectCount === $maxRedirects) { - throw new \LogicException(sprintf( - 'The maximum number (%d) of redirections was reached.', - $maxRedirects - )); - } - - $this->debugSection('Redirecting to', $locationHeader); - - $result = $this->client->followRedirect(); - $this->debugResponse($locationHeader); - return $this->redirectIfNecessary($result, $maxRedirects, $redirectCount + 1); - } - $this->client->followRedirects(true); - return $result; - } - /** * Switch to iframe or frame on the page. * @@ -1963,15 +2078,15 @@ public function switchToIframe($name) public function moveBack($numberOfSteps = 1) { if (!is_int($numberOfSteps) || $numberOfSteps < 1) { - throw new \InvalidArgumentException('numberOfSteps must be positive integer'); + throw new InvalidArgumentException('numberOfSteps must be positive integer'); } try { $history = $this->getRunningClient()->getHistory(); for ($i = $numberOfSteps; $i > 0; $i--) { $request = $history->back(); } - } catch (\LogicException $e) { - throw new \InvalidArgumentException( + } catch (LogicException $e) { + throw new InvalidArgumentException( sprintf( 'numberOfSteps is set to %d, but there are only %d previous steps in the history', $numberOfSteps, @@ -1989,68 +2104,6 @@ public function moveBack($numberOfSteps = 1) ); } - protected function debugCookieJar() - { - $cookies = $this->client->getCookieJar()->all(); - $cookieStrings = array_map('strval', $cookies); - $this->debugSection('Cookie Jar', $cookieStrings); - } - - protected function setCookiesFromOptions() - { - if (isset($this->config['cookies']) && is_array($this->config['cookies']) && !empty($this->config['cookies'])) { - $domain = parse_url($this->config['url'], PHP_URL_HOST); - $cookieJar = $this->client->getCookieJar(); - foreach ($this->config['cookies'] as &$cookie) { - if (!is_array($cookie) || !array_key_exists('Name', $cookie) || !array_key_exists('Value', $cookie)) { - throw new \InvalidArgumentException('Cookies must have at least Name and Value attributes'); - } - if (!isset($cookie['Domain'])) { - $cookie['Domain'] = $domain; - } - if (!isset($cookie['Expires'])) { - $cookie['Expires'] = null; - } - if (!isset($cookie['Path'])) { - $cookie['Path'] = '/'; - } - if (!isset($cookie['Secure'])) { - $cookie['Secure'] = false; - } - if (!isset($cookie['HttpOnly'])) { - $cookie['HttpOnly'] = false; - } - $cookieJar->set(new \Symfony\Component\BrowserKit\Cookie( - $cookie['Name'], - $cookie['Value'], - $cookie['Expires'], - $cookie['Path'], - $cookie['Domain'], - $cookie['Secure'], - $cookie['HttpOnly'] - )); - } - } - } - - /** - * @return string - */ - protected function getNormalizedResponseContent() - { - $content = $this->_getResponseContent(); - // Since strip_tags has problems with JS code that contains - // an <= operator the script tags have to be removed manually first. - $content = preg_replace('#(.*?)#is', '', $content); - - $content = strip_tags($content); - $content = html_entity_decode($content, ENT_QUOTES); - $content = str_replace("\n", ' ', $content); - $content = preg_replace('/\s{2,}/', ' ', $content); - - return $content; - } - /** * Sets SERVER parameters valid for all next requests. * this will remove old ones. From da5bab00b4940649fd6f3811b5cb4f17fba7d68c Mon Sep 17 00:00:00 2001 From: Tavo Nieves J Date: Sun, 11 Oct 2020 19:42:23 -0500 Subject: [PATCH 4/7] added traits for each implemented interface --- src/Codeception/Lib/InnerBrowser.php | 542 +----------------- .../Lib/Model/ConflictsWithModuleTrait.php | 17 + .../Lib/Model/ElementLocatorTrait.php | 16 + .../Lib/Model/PageSourceSaverTrait.php | 31 + src/Codeception/Lib/Model/WebTrait.php | 516 +++++++++++++++++ 5 files changed, 591 insertions(+), 531 deletions(-) create mode 100644 src/Codeception/Lib/Model/ConflictsWithModuleTrait.php create mode 100644 src/Codeception/Lib/Model/ElementLocatorTrait.php create mode 100644 src/Codeception/Lib/Model/PageSourceSaverTrait.php create mode 100644 src/Codeception/Lib/Model/WebTrait.php diff --git a/src/Codeception/Lib/InnerBrowser.php b/src/Codeception/Lib/InnerBrowser.php index 04952ff..cf99680 100644 --- a/src/Codeception/Lib/InnerBrowser.php +++ b/src/Codeception/Lib/InnerBrowser.php @@ -10,6 +10,10 @@ use Codeception\Lib\Interfaces\ElementLocator; use Codeception\Lib\Interfaces\PageSourceSaver; use Codeception\Lib\Interfaces\Web; +use Codeception\Lib\Model\ConflictsWithModuleTrait; +use Codeception\Lib\Model\ElementLocatorTrait; +use Codeception\Lib\Model\PageSourceSaverTrait; +use Codeception\Lib\Model\WebTrait; use Codeception\Module; use Codeception\PHPUnit\Constraint\Crawler as CrawlerConstraint; use Codeception\PHPUnit\Constraint\CrawlerNot as CrawlerNotConstraint; @@ -43,6 +47,13 @@ class_alias('Symfony\Component\BrowserKit\Client', 'Symfony\Component\BrowserKit class InnerBrowser extends Module implements Web, PageSourceSaver, ElementLocator, ConflictsWithModule { + use + ConflictsWithModuleTrait, + ElementLocatorTrait, + PageSourceSaverTrait, + WebTrait + ; + private $baseUrl; /** @@ -1168,16 +1179,6 @@ public function _after(TestInterface $test) $this->headers = []; } - public function _conflicts() - { - return 'Codeception\Lib\Interfaces\Web'; - } - - public function _findElements($locator) - { - return $this->match($locator); - } - /** * Send custom request to a backend using method, uri, parameters, etc. * Use it in Helpers to create special request actions, like accessing API @@ -1276,11 +1277,6 @@ public function _loadPage( $this->forms = []; } - public function _savePageSource($filename) - { - file_put_contents($filename, $this->_getResponseContent()); - } - /** * Authenticates user for HTTP_AUTH * @@ -1349,108 +1345,6 @@ public function deleteHeader($name) unset($this->headers[$name]); } - - public function amOnPage($page) - { - $this->_loadPage('GET', $page); - } - - public function click($link, $context = null) - { - if ($context) { - $this->crawler = $this->match($context); - } - - if (is_array($link)) { - $this->clickByLocator($link); - return; - } - - $anchor = $this->strictMatch(['link' => $link]); - if (!count($anchor)) { - $anchor = $this->getCrawler()->selectLink($link); - } - if (count($anchor)) { - $this->openHrefFromDomNode($anchor->getNode(0)); - return; - } - - $buttonText = str_replace('"', "'", $link); - $button = $this->crawler->selectButton($buttonText); - - if (count($button) && $this->clickButton($button->getNode(0))) { - return; - } - - try { - $this->clickByLocator($link); - } catch (MalformedLocatorException $e) { - throw new ElementNotFound("name=$link", "'$link' is invalid CSS and XPath selector and Link or Button"); - } - } - - public function see($text, $selector = null) - { - if (!$selector) { - $this->assertPageContains($text); - return; - } - - $nodes = $this->match($selector); - $this->assertDomContains($nodes, $this->stringifySelector($selector), $text); - } - - public function dontSee($text, $selector = null) - { - if (!$selector) { - $this->assertPageNotContains($text); - return; - } - - $nodes = $this->match($selector); - $this->assertDomNotContains($nodes, $this->stringifySelector($selector), $text); - } - - public function seeInSource($raw) - { - $this->assertPageSourceContains($raw); - } - - public function dontSeeInSource($raw) - { - $this->assertPageSourceNotContains($raw); - } - - public function seeLink($text, $url = null) - { - $crawler = $this->getCrawler()->selectLink($text); - if ($crawler->count() === 0) { - $this->fail("No links containing text '$text' were found in page " . $this->_getCurrentUri()); - } - if ($url) { - $crawler = $crawler->filterXPath(sprintf('.//a[substring(@href, string-length(@href) - string-length(%1$s) + 1)=%1$s]', Crawler::xpathLiteral($url))); - if ($crawler->count() === 0) { - $this->fail("No links containing text '$text' and URL '$url' were found in page " . $this->_getCurrentUri()); - } - } - $this->assertTrue(true); - } - - public function dontSeeLink($text, $url = '') - { - $crawler = $this->getCrawler()->selectLink($text); - if (!$url && $crawler->count() > 0) { - $this->fail("Link containing text '$text' was found in page " . $this->_getCurrentUri()); - } - $crawler = $crawler->filterXPath( - sprintf('.//a[substring(@href, string-length(@href) - string-length(%1$s) + 1)=%1$s]', - Crawler::xpathLiteral($url)) - ); - if ($crawler->count() > 0) { - $this->fail("Link containing text '$text' and URL '$url' was found in page " . $this->_getCurrentUri()); - } - } - /** * @return string * @throws ModuleException @@ -1460,182 +1354,6 @@ public function _getCurrentUri() return Uri::retrieveUri($this->getRunningClient()->getHistory()->current()->getUri()); } - public function seeInCurrentUrl($uri) - { - $this->assertStringContainsString($uri, $this->_getCurrentUri()); - } - - public function dontSeeInCurrentUrl($uri) - { - $this->assertStringNotContainsString($uri, $this->_getCurrentUri()); - } - - public function seeCurrentUrlEquals($uri) - { - $this->assertEquals(rtrim($uri, '/'), rtrim($this->_getCurrentUri(), '/')); - } - - public function dontSeeCurrentUrlEquals($uri) - { - $this->assertNotEquals(rtrim($uri, '/'), rtrim($this->_getCurrentUri(), '/')); - } - - public function seeCurrentUrlMatches($uri) - { - $this->assertRegExp($uri, $this->_getCurrentUri()); - } - - public function dontSeeCurrentUrlMatches($uri) - { - $this->assertNotRegExp($uri, $this->_getCurrentUri()); - } - - public function grabFromCurrentUrl($uri = null) - { - if (!$uri) { - return $this->_getCurrentUri(); - } - $matches = []; - $res = preg_match($uri, $this->_getCurrentUri(), $matches); - if (!$res) { - $this->fail("Couldn't match $uri in " . $this->_getCurrentUri()); - } - if (!isset($matches[1])) { - $this->fail("Nothing to grab. A regex parameter required. Ex: '/user/(\\d+)'"); - } - return $matches[1]; - } - - public function seeCheckboxIsChecked($checkbox) - { - $checkboxes = $this->getFieldsByLabelOrCss($checkbox); - $this->assertDomContains($checkboxes->filter('input[checked=checked]'), 'checkbox'); - } - - public function dontSeeCheckboxIsChecked($checkbox) - { - $checkboxes = $this->getFieldsByLabelOrCss($checkbox); - $this->assertEquals(0, $checkboxes->filter('input[checked=checked]')->count()); - } - - public function seeInField($field, $value) - { - $nodes = $this->getFieldsByLabelOrCss($field); - $this->assert($this->proceedSeeInField($nodes, $value)); - } - - public function dontSeeInField($field, $value) - { - $nodes = $this->getFieldsByLabelOrCss($field); - $this->assertNot($this->proceedSeeInField($nodes, $value)); - } - - public function seeInFormFields($formSelector, array $params) - { - $this->proceedSeeInFormFields($formSelector, $params, false); - } - - public function dontSeeInFormFields($formSelector, array $params) - { - $this->proceedSeeInFormFields($formSelector, $params, true); - } - - public function submitForm($selector, array $params, $button = null) - { - $form = $this->match($selector)->first(); - if (!count($form)) { - throw new ElementNotFound($this->stringifySelector($selector), 'Form'); - } - $this->proceedSubmitForm($form, $params, $button); - } - - public function fillField($field, $value) - { - $input = $this->getFieldByLabelOrCss($field); - $form = $this->getFormFor($input); - $name = $input->attr('name'); - - $dynamicField = $input->getNode(0)->tagName === 'textarea' - ? new TextareaFormField($input->getNode(0)) - : new InputFormField($input->getNode(0)); - $formField = $this->matchFormField($name, $form, $dynamicField); - $formField->setValue($value); - $input->getNode(0)->setAttribute('value', htmlspecialchars($value)); - if ($input->getNode(0)->tagName === 'textarea') { - $input->getNode(0)->nodeValue = htmlspecialchars($value); - } - } - - public function selectOption($select, $option) - { - $field = $this->getFieldByLabelOrCss($select); - $form = $this->getFormFor($field); - $fieldName = $this->getSubmissionFormFieldName($field->attr('name')); - - if (is_array($option)) { - if (!isset($option[0])) { // strict option locator - $form[$fieldName]->select($this->matchOption($field, $option)); - codecept_debug($option); - return; - } - $options = []; - foreach ($option as $opt) { - $options[] = $this->matchOption($field, $opt); - } - $form[$fieldName]->select($options); - return; - } - - $dynamicField = new ChoiceFormField($field->getNode(0)); - $formField = $this->matchFormField($fieldName, $form, $dynamicField); - $selValue = $this->matchOption($field, $option); - - if (is_array($formField)) { - foreach ($formField as $field) { - $values = $field->availableOptionValues(); - foreach ($values as $val) { - if ($val === $option) { - $field->select($selValue); - return; - } - } - } - return; - } - - $formField->select($this->matchOption($field, $option)); - } - - public function checkOption($option) - { - $this->proceedCheckOption($option)->tick(); - } - - public function uncheckOption($option) - { - $this->proceedCheckOption($option)->untick(); - } - - public function attachFile($field, $filename) - { - $form = $this->getFormFor($field = $this->getFieldByLabelOrCss($field)); - $filePath = codecept_data_dir() . $filename; - if (!file_exists($filePath)) { - throw new InvalidArgumentException("File does not exist: $filePath"); - } - if (!is_readable($filePath)) { - throw new InvalidArgumentException("File is not readable: $filePath"); - } - - $name = $field->attr('name'); - $formField = $this->matchFormField($name, $form, new FileFormField($field->getNode(0))); - if (is_array($formField)) { - $this->fail("Field $name is ignored on upload, field $name is treated as array."); - } - - $formField->upload($filePath); - } - /** * Sends an ajax GET request with the passed parameters. * See `sendAjaxPostRequest()` @@ -1694,230 +1412,11 @@ public function sendAjaxRequest($method, $uri, $params = []) $this->clientRequest($method, $uri, $params, [], ['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'], null, false); } - public function makeHtmlSnapshot($name = null) - { - if (empty($name)) { - $name = uniqid(date("Y-m-d_H-i-s_"), true); - } - $debugDir = codecept_output_dir() . 'debug'; - if (!is_dir($debugDir)) { - mkdir($debugDir, 0777); - } - $fileName = $debugDir . DIRECTORY_SEPARATOR . $name . '.html'; - - $this->_savePageSource($fileName); - $this->debugSection('Snapshot Saved', "file://$fileName"); - } - public function _getResponseStatusCode() { return $this->getResponseStatusCode(); } - public function grabTextFrom($cssOrXPathOrRegex) - { - if (@preg_match($cssOrXPathOrRegex, $this->client->getInternalResponse()->getContent(), $matches)) { - return $matches[1]; - } - $nodes = $this->match($cssOrXPathOrRegex); - if ($nodes->count()) { - return $nodes->first()->text(); - } - throw new ElementNotFound($cssOrXPathOrRegex, 'Element that matches CSS or XPath or Regex'); - } - - public function grabAttributeFrom($cssOrXpath, $attribute) - { - $nodes = $this->match($cssOrXpath); - if (!$nodes->count()) { - throw new ElementNotFound($cssOrXpath, 'Element that matches CSS or XPath'); - } - return $nodes->first()->attr($attribute); - } - - public function grabMultiple($cssOrXpath, $attribute = null) - { - $result = []; - $nodes = $this->match($cssOrXpath); - - foreach ($nodes as $node) { - if ($attribute !== null) { - $result[] = $node->getAttribute($attribute); - } else { - $result[] = $node->textContent; - } - } - return $result; - } - - /** - * @param $field - * - * @return array|mixed|null|string - */ - public function grabValueFrom($field) - { - $nodes = $this->match($field); - if (!$nodes->count()) { - throw new ElementNotFound($field, 'Field'); - } - - if ($nodes->filter('textarea')->count()) { - return (new TextareaFormField($nodes->filter('textarea')->getNode(0)))->getValue(); - } - - $input = $nodes->filter('input'); - if ($input->count()) { - return $this->getInputValue($input); - } - - if ($nodes->filter('select')->count()) { - $field = new ChoiceFormField($nodes->filter('select')->getNode(0)); - $options = $nodes->filter('option[selected]'); - $values = []; - - foreach ($options as $option) { - $values[] = $option->getAttribute('value'); - } - - if (!$field->isMultiple()) { - return reset($values); - } - - return $values; - } - - $this->fail("Element $nodes is not a form field or does not contain a form field"); - } - - public function setCookie($name, $val, array $params = []) - { - $cookies = $this->client->getCookieJar(); - $params = array_merge($this->defaultCookieParameters, $params); - - $expires = isset($params['expiry']) ? $params['expiry'] : null; // WebDriver compatibility - $expires = isset($params['expires']) && !$expires ? $params['expires'] : null; - $path = isset($params['path']) ? $params['path'] : null; - $domain = isset($params['domain']) ? $params['domain'] : ''; - $secure = isset($params['secure']) ? $params['secure'] : false; - $httpOnly = isset($params['httpOnly']) ? $params['httpOnly'] : true; - $encodedValue = isset($params['encodedValue']) ? $params['encodedValue'] : false; - - - - $cookies->set(new Cookie($name, $val, $expires, $path, $domain, $secure, $httpOnly, $encodedValue)); - $this->debugCookieJar(); - } - - public function grabCookie($cookie, array $params = []) - { - $params = array_merge($this->defaultCookieParameters, $params); - $this->debugCookieJar(); - $cookies = $this->getRunningClient()->getCookieJar()->get($cookie, $params['path'], $params['domain']); - if (!$cookies) { - return null; - } - return $cookies->getValue(); - } - - /** - * Grabs current page source code. - * - * @throws ModuleException if no page was opened. - * - * @return string Current page source code. - */ - public function grabPageSource() - { - return $this->_getResponseContent(); - } - - public function seeCookie($cookie, array $params = []) - { - $params = array_merge($this->defaultCookieParameters, $params); - $this->debugCookieJar(); - $this->assertNotNull($this->client->getCookieJar()->get($cookie, $params['path'], $params['domain'])); - } - - public function dontSeeCookie($cookie, array $params = []) - { - $params = array_merge($this->defaultCookieParameters, $params); - $this->debugCookieJar(); - $this->assertNull($this->client->getCookieJar()->get($cookie, $params['path'], $params['domain'])); - } - - public function resetCookie($name, array $params = []) - { - $params = array_merge($this->defaultCookieParameters, $params); - $this->client->getCookieJar()->expire($name, $params['path'], $params['domain']); - $this->debugCookieJar(); - } - - public function seeElement($selector, $attributes = []) - { - $nodes = $this->match($selector); - $selector = $this->stringifySelector($selector); - if (!empty($attributes)) { - $nodes = $this->filterByAttributes($nodes, $attributes); - $selector .= "' with attribute(s) '" . trim(json_encode($attributes), '{}'); - } - $this->assertDomContains($nodes, $selector); - } - - public function dontSeeElement($selector, $attributes = []) - { - $nodes = $this->match($selector); - $selector = $this->stringifySelector($selector); - if (!empty($attributes)) { - $nodes = $this->filterByAttributes($nodes, $attributes); - $selector .= "' with attribute(s) '" . trim(json_encode($attributes), '{}'); - } - $this->assertDomNotContains($nodes, $selector); - } - - public function seeNumberOfElements($selector, $expected) - { - $counted = count($this->match($selector)); - if (is_array($expected)) { - list($floor, $ceil) = $expected; - $this->assertTrue( - $floor <= $counted && $ceil >= $counted, - 'Number of elements counted differs from expected range' - ); - } else { - $this->assertEquals( - $expected, - $counted, - 'Number of elements counted differs from expected number' - ); - } - } - - public function seeOptionIsSelected($selector, $optionText) - { - $selected = $this->matchSelectedOption($selector); - $this->assertDomContains($selected, 'selected option'); - //If element is radio then we need to check value - $value = $selected->getNode(0)->tagName === 'option' - ? $selected->text() - : $selected->getNode(0)->getAttribute('value'); - $this->assertEquals($optionText, $value); - } - - public function dontSeeOptionIsSelected($selector, $optionText) - { - $selected = $this->matchSelectedOption($selector); - if (!$selected->count()) { - $this->assertEquals(0, $selected->count()); - return; - } - //If element is radio then we need to check value - $value = $selected->getNode(0)->tagName === 'option' - ? $selected->text() - : $selected->getNode(0)->getAttribute('value'); - $this->assertNotEquals($optionText, $value); - } - /** * Asserts that current page has 404 response status code. */ @@ -2020,25 +1519,6 @@ public function seeResponseCodeIsServerError() $this->seeResponseCodeIsBetween(500, 599); } - public function seeInTitle($title) - { - $nodes = $this->getCrawler()->filter('title'); - if (!$nodes->count()) { - throw new ElementNotFound("", "Tag"); - } - $this->assertStringContainsString($title, $nodes->first()->text(), "page title contains $title"); - } - - public function dontSeeInTitle($title) - { - $nodes = $this->getCrawler()->filter('title'); - if (!$nodes->count()) { - $this->assertTrue(true); - return; - } - $this->assertStringNotContainsString($title, $nodes->first()->text(), "page title contains $title"); - } - /** * Switch to iframe or frame on the page. * diff --git a/src/Codeception/Lib/Model/ConflictsWithModuleTrait.php b/src/Codeception/Lib/Model/ConflictsWithModuleTrait.php new file mode 100644 index 0000000..41976de --- /dev/null +++ b/src/Codeception/Lib/Model/ConflictsWithModuleTrait.php @@ -0,0 +1,17 @@ +<?php + +namespace Codeception\Lib\Model; + +use Codeception\Lib\Interfaces\ConflictsWithModule; +use Codeception\Lib\Interfaces\Web; + +/** + * @see ConflictsWithModule + */ +trait ConflictsWithModuleTrait +{ + public function _conflicts() + { + return Web::class; + } +} diff --git a/src/Codeception/Lib/Model/ElementLocatorTrait.php b/src/Codeception/Lib/Model/ElementLocatorTrait.php new file mode 100644 index 0000000..3508e47 --- /dev/null +++ b/src/Codeception/Lib/Model/ElementLocatorTrait.php @@ -0,0 +1,16 @@ +<?php + +namespace Codeception\Lib\Model; + +use Codeception\Lib\Interfaces\ElementLocator; + +/** + * @see ElementLocator + */ +trait ElementLocatorTrait +{ + public function _findElements($locator) + { + return $this->match($locator); + } +} \ No newline at end of file diff --git a/src/Codeception/Lib/Model/PageSourceSaverTrait.php b/src/Codeception/Lib/Model/PageSourceSaverTrait.php new file mode 100644 index 0000000..db25428 --- /dev/null +++ b/src/Codeception/Lib/Model/PageSourceSaverTrait.php @@ -0,0 +1,31 @@ +<?php + +namespace Codeception\Lib\Model; + +use Codeception\Lib\Interfaces\PageSourceSaver; + +/** + * @see PageSourceSaver + */ +trait PageSourceSaverTrait +{ + public function _savePageSource($filename) + { + file_put_contents($filename, $this->_getResponseContent()); + } + + public function makeHtmlSnapshot($name = null) + { + if (empty($name)) { + $name = uniqid(date("Y-m-d_H-i-s_"), true); + } + $debugDir = codecept_output_dir() . 'debug'; + if (!is_dir($debugDir)) { + mkdir($debugDir, 0777); + } + $fileName = $debugDir . DIRECTORY_SEPARATOR . $name . '.html'; + + $this->_savePageSource($fileName); + $this->debugSection('Snapshot Saved', "file://$fileName"); + } +} \ No newline at end of file diff --git a/src/Codeception/Lib/Model/WebTrait.php b/src/Codeception/Lib/Model/WebTrait.php new file mode 100644 index 0000000..18f63c6 --- /dev/null +++ b/src/Codeception/Lib/Model/WebTrait.php @@ -0,0 +1,516 @@ +<?php + +namespace Codeception\Lib\Model; + +use Codeception\Exception\ElementNotFound; +use Codeception\Exception\MalformedLocatorException; +use Codeception\Lib\Interfaces\Web; +use InvalidArgumentException; +use Symfony\Component\BrowserKit\Cookie; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\DomCrawler\Field\ChoiceFormField; +use Symfony\Component\DomCrawler\Field\FileFormField; +use Symfony\Component\DomCrawler\Field\InputFormField; +use Symfony\Component\DomCrawler\Field\TextareaFormField; + +/** + * @see Web + */ +trait WebTrait +{ + public function amOnPage($page) + { + $this->_loadPage('GET', $page); + } + + public function see($text, $selector = null) + { + if (!$selector) { + $this->assertPageContains($text); + return; + } + + $nodes = $this->match($selector); + $this->assertDomContains($nodes, $this->stringifySelector($selector), $text); + } + + public function dontSee($text, $selector = null) + { + if (!$selector) { + $this->assertPageNotContains($text); + return; + } + + $nodes = $this->match($selector); + $this->assertDomNotContains($nodes, $this->stringifySelector($selector), $text); + } + + public function seeInSource($raw) + { + $this->assertPageSourceContains($raw); + } + + public function dontSeeInSource($raw) + { + $this->assertPageSourceNotContains($raw); + } + + public function submitForm($selector, array $params, $button = null) + { + $form = $this->match($selector)->first(); + if (!count($form)) { + throw new ElementNotFound($this->stringifySelector($selector), 'Form'); + } + $this->proceedSubmitForm($form, $params, $button); + } + + public function click($link, $context = null) + { + if ($context) { + $this->crawler = $this->match($context); + } + + if (is_array($link)) { + $this->clickByLocator($link); + return; + } + + $anchor = $this->strictMatch(['link' => $link]); + if (!count($anchor)) { + $anchor = $this->getCrawler()->selectLink($link); + } + if (count($anchor)) { + $this->openHrefFromDomNode($anchor->getNode(0)); + return; + } + + $buttonText = str_replace('"', "'", $link); + $button = $this->crawler->selectButton($buttonText); + + if (count($button) && $this->clickButton($button->getNode(0))) { + return; + } + + try { + $this->clickByLocator($link); + } catch (MalformedLocatorException $e) { + throw new ElementNotFound("name=$link", "'$link' is invalid CSS and XPath selector and Link or Button"); + } + } + + public function seeLink($text, $url = null) + { + $crawler = $this->getCrawler()->selectLink($text); + if ($crawler->count() === 0) { + $this->fail("No links containing text '$text' were found in page " . $this->_getCurrentUri()); + } + if ($url) { + $crawler = $crawler->filterXPath(sprintf('.//a[substring(@href, string-length(@href) - string-length(%1$s) + 1)=%1$s]', Crawler::xpathLiteral($url))); + if ($crawler->count() === 0) { + $this->fail("No links containing text '$text' and URL '$url' were found in page " . $this->_getCurrentUri()); + } + } + $this->assertTrue(true); + } + + public function dontSeeLink($text, $url = '') + { + $crawler = $this->getCrawler()->selectLink($text); + if (!$url && $crawler->count() > 0) { + $this->fail("Link containing text '$text' was found in page " . $this->_getCurrentUri()); + } + $crawler = $crawler->filterXPath( + sprintf('.//a[substring(@href, string-length(@href) - string-length(%1$s) + 1)=%1$s]', + Crawler::xpathLiteral($url)) + ); + if ($crawler->count() > 0) { + $this->fail("Link containing text '$text' and URL '$url' was found in page " . $this->_getCurrentUri()); + } + } + + public function seeInCurrentUrl($uri) + { + $this->assertStringContainsString($uri, $this->_getCurrentUri()); + } + + public function seeCurrentUrlEquals($uri) + { + $this->assertEquals(rtrim($uri, '/'), rtrim($this->_getCurrentUri(), '/')); + } + + public function seeCurrentUrlMatches($uri) + { + $this->assertRegExp($uri, $this->_getCurrentUri()); + } + + public function dontSeeInCurrentUrl($uri) + { + $this->assertStringNotContainsString($uri, $this->_getCurrentUri()); + } + + public function dontSeeCurrentUrlEquals($uri) + { + $this->assertNotEquals(rtrim($uri, '/'), rtrim($this->_getCurrentUri(), '/')); + } + + public function dontSeeCurrentUrlMatches($uri) + { + $this->assertNotRegExp($uri, $this->_getCurrentUri()); + } + + public function grabFromCurrentUrl($uri = null) + { + if (!$uri) { + return $this->_getCurrentUri(); + } + $matches = []; + $res = preg_match($uri, $this->_getCurrentUri(), $matches); + if (!$res) { + $this->fail("Couldn't match $uri in " . $this->_getCurrentUri()); + } + if (!isset($matches[1])) { + $this->fail("Nothing to grab. A regex parameter required. Ex: '/user/(\\d+)'"); + } + return $matches[1]; + } + + public function seeCheckboxIsChecked($checkbox) + { + $checkboxes = $this->getFieldsByLabelOrCss($checkbox); + $this->assertDomContains($checkboxes->filter('input[checked=checked]'), 'checkbox'); + } + + public function dontSeeCheckboxIsChecked($checkbox) + { + $checkboxes = $this->getFieldsByLabelOrCss($checkbox); + $this->assertEquals(0, $checkboxes->filter('input[checked=checked]')->count()); + } + + public function seeInField($field, $value) + { + $nodes = $this->getFieldsByLabelOrCss($field); + $this->assert($this->proceedSeeInField($nodes, $value)); + } + + public function dontSeeInField($field, $value) + { + $nodes = $this->getFieldsByLabelOrCss($field); + $this->assertNot($this->proceedSeeInField($nodes, $value)); + } + + public function seeInFormFields($formSelector, array $params) + { + $this->proceedSeeInFormFields($formSelector, $params, false); + } + + public function dontSeeInFormFields($formSelector, array $params) + { + $this->proceedSeeInFormFields($formSelector, $params, true); + } + + public function selectOption($select, $option) + { + $field = $this->getFieldByLabelOrCss($select); + $form = $this->getFormFor($field); + $fieldName = $this->getSubmissionFormFieldName($field->attr('name')); + + if (is_array($option)) { + if (!isset($option[0])) { // strict option locator + $form[$fieldName]->select($this->matchOption($field, $option)); + codecept_debug($option); + return; + } + $options = []; + foreach ($option as $opt) { + $options[] = $this->matchOption($field, $opt); + } + $form[$fieldName]->select($options); + return; + } + + $dynamicField = new ChoiceFormField($field->getNode(0)); + $formField = $this->matchFormField($fieldName, $form, $dynamicField); + $selValue = $this->matchOption($field, $option); + + if (is_array($formField)) { + foreach ($formField as $field) { + $values = $field->availableOptionValues(); + foreach ($values as $val) { + if ($val === $option) { + $field->select($selValue); + return; + } + } + } + return; + } + + $formField->select($this->matchOption($field, $option)); + } + + public function checkOption($option) + { + $this->proceedCheckOption($option)->tick(); + } + + public function uncheckOption($option) + { + $this->proceedCheckOption($option)->untick(); + } + + public function fillField($field, $value) + { + $input = $this->getFieldByLabelOrCss($field); + $form = $this->getFormFor($input); + $name = $input->attr('name'); + + $dynamicField = $input->getNode(0)->tagName === 'textarea' + ? new TextareaFormField($input->getNode(0)) + : new InputFormField($input->getNode(0)); + $formField = $this->matchFormField($name, $form, $dynamicField); + $formField->setValue($value); + $input->getNode(0)->setAttribute('value', htmlspecialchars($value)); + if ($input->getNode(0)->tagName === 'textarea') { + $input->getNode(0)->nodeValue = htmlspecialchars($value); + } + } + + public function attachFile($field, $filename) + { + $form = $this->getFormFor($field = $this->getFieldByLabelOrCss($field)); + $filePath = codecept_data_dir() . $filename; + if (!file_exists($filePath)) { + throw new InvalidArgumentException("File does not exist: $filePath"); + } + if (!is_readable($filePath)) { + throw new InvalidArgumentException("File is not readable: $filePath"); + } + + $name = $field->attr('name'); + $formField = $this->matchFormField($name, $form, new FileFormField($field->getNode(0))); + if (is_array($formField)) { + $this->fail("Field $name is ignored on upload, field $name is treated as array."); + } + + $formField->upload($filePath); + } + + public function grabTextFrom($cssOrXPathOrRegex) + { + if (@preg_match($cssOrXPathOrRegex, $this->client->getInternalResponse()->getContent(), $matches)) { + return $matches[1]; + } + $nodes = $this->match($cssOrXPathOrRegex); + if ($nodes->count()) { + return $nodes->first()->text(); + } + throw new ElementNotFound($cssOrXPathOrRegex, 'Element that matches CSS or XPath or Regex'); + } + + /** + * @param $field + * + * @return array|mixed|null|string + */ + public function grabValueFrom($field) + { + $nodes = $this->match($field); + if (!$nodes->count()) { + throw new ElementNotFound($field, 'Field'); + } + + if ($nodes->filter('textarea')->count()) { + return (new TextareaFormField($nodes->filter('textarea')->getNode(0)))->getValue(); + } + + $input = $nodes->filter('input'); + if ($input->count()) { + return $this->getInputValue($input); + } + + if ($nodes->filter('select')->count()) { + $field = new ChoiceFormField($nodes->filter('select')->getNode(0)); + $options = $nodes->filter('option[selected]'); + $values = []; + + foreach ($options as $option) { + $values[] = $option->getAttribute('value'); + } + + if (!$field->isMultiple()) { + return reset($values); + } + + return $values; + } + + $this->fail("Element $nodes is not a form field or does not contain a form field"); + } + + public function grabAttributeFrom($cssOrXpath, $attribute) + { + $nodes = $this->match($cssOrXpath); + if (!$nodes->count()) { + throw new ElementNotFound($cssOrXpath, 'Element that matches CSS or XPath'); + } + return $nodes->first()->attr($attribute); + } + + public function grabMultiple($cssOrXpath, $attribute = null) + { + $result = []; + $nodes = $this->match($cssOrXpath); + + foreach ($nodes as $node) { + if ($attribute !== null) { + $result[] = $node->getAttribute($attribute); + } else { + $result[] = $node->textContent; + } + } + return $result; + } + + public function seeElement($selector, $attributes = []) + { + $nodes = $this->match($selector); + $selector = $this->stringifySelector($selector); + if (!empty($attributes)) { + $nodes = $this->filterByAttributes($nodes, $attributes); + $selector .= "' with attribute(s) '" . trim(json_encode($attributes), '{}'); + } + $this->assertDomContains($nodes, $selector); + } + + public function dontSeeElement($selector, $attributes = []) + { + $nodes = $this->match($selector); + $selector = $this->stringifySelector($selector); + if (!empty($attributes)) { + $nodes = $this->filterByAttributes($nodes, $attributes); + $selector .= "' with attribute(s) '" . trim(json_encode($attributes), '{}'); + } + $this->assertDomNotContains($nodes, $selector); + } + + public function seeNumberOfElements($selector, $expected) + { + $counted = count($this->match($selector)); + if (is_array($expected)) { + list($floor, $ceil) = $expected; + $this->assertTrue( + $floor <= $counted && $ceil >= $counted, + 'Number of elements counted differs from expected range' + ); + } else { + $this->assertEquals( + $expected, + $counted, + 'Number of elements counted differs from expected number' + ); + } + } + + public function seeOptionIsSelected($selector, $optionText) + { + $selected = $this->matchSelectedOption($selector); + $this->assertDomContains($selected, 'selected option'); + //If element is radio then we need to check value + $value = $selected->getNode(0)->tagName === 'option' + ? $selected->text() + : $selected->getNode(0)->getAttribute('value'); + $this->assertEquals($optionText, $value); + } + + public function dontSeeOptionIsSelected($selector, $optionText) + { + $selected = $this->matchSelectedOption($selector); + if (!$selected->count()) { + $this->assertEquals(0, $selected->count()); + return; + } + //If element is radio then we need to check value + $value = $selected->getNode(0)->tagName === 'option' + ? $selected->text() + : $selected->getNode(0)->getAttribute('value'); + $this->assertNotEquals($optionText, $value); + } + + public function seeInTitle($title) + { + $nodes = $this->getCrawler()->filter('title'); + if (!$nodes->count()) { + throw new ElementNotFound("<title>", "Tag"); + } + $this->assertStringContainsString($title, $nodes->first()->text(), "page title contains $title"); + } + + public function dontSeeInTitle($title) + { + $nodes = $this->getCrawler()->filter('title'); + if (!$nodes->count()) { + $this->assertTrue(true); + return; + } + $this->assertStringNotContainsString($title, $nodes->first()->text(), "page title contains $title"); + } + + public function seeCookie($cookie, array $params = []) + { + $params = array_merge($this->defaultCookieParameters, $params); + $this->debugCookieJar(); + $this->assertNotNull($this->client->getCookieJar()->get($cookie, $params['path'], $params['domain'])); + } + + public function dontSeeCookie($cookie, array $params = []) + { + $params = array_merge($this->defaultCookieParameters, $params); + $this->debugCookieJar(); + $this->assertNull($this->client->getCookieJar()->get($cookie, $params['path'], $params['domain'])); + } + + public function setCookie($name, $val, array $params = []) + { + $cookies = $this->client->getCookieJar(); + $params = array_merge($this->defaultCookieParameters, $params); + + $expires = isset($params['expiry']) ? $params['expiry'] : null; // WebDriver compatibility + $expires = isset($params['expires']) && !$expires ? $params['expires'] : null; + $path = isset($params['path']) ? $params['path'] : null; + $domain = isset($params['domain']) ? $params['domain'] : ''; + $secure = isset($params['secure']) ? $params['secure'] : false; + $httpOnly = isset($params['httpOnly']) ? $params['httpOnly'] : true; + $encodedValue = isset($params['encodedValue']) ? $params['encodedValue'] : false; + + $cookies->set(new Cookie($name, $val, $expires, $path, $domain, $secure, $httpOnly, $encodedValue)); + $this->debugCookieJar(); + } + + public function resetCookie($name, array $params = []) + { + $params = array_merge($this->defaultCookieParameters, $params); + $this->client->getCookieJar()->expire($name, $params['path'], $params['domain']); + $this->debugCookieJar(); + } + + public function grabCookie($cookie, array $params = []) + { + $params = array_merge($this->defaultCookieParameters, $params); + $this->debugCookieJar(); + $cookies = $this->getRunningClient()->getCookieJar()->get($cookie, $params['path'], $params['domain']); + if (!$cookies) { + return null; + } + return $cookies->getValue(); + } + + /** + * Grabs current page source code. + * + * @return string Current page source code. + */ + public function grabPageSource() + { + return $this->_getResponseContent(); + } +} \ No newline at end of file From e4ff8fe4cf0e3ca03fbbcb5bd47007b3c549da05 Mon Sep 17 00:00:00 2001 From: Tavo Nieves J <ganieves@outlook.com> Date: Sun, 11 Oct 2020 19:46:52 -0500 Subject: [PATCH 5/7] public functions were rearranged --- src/Codeception/Lib/InnerBrowser.php | 418 ++++++++++++++------------- 1 file changed, 211 insertions(+), 207 deletions(-) diff --git a/src/Codeception/Lib/InnerBrowser.php b/src/Codeception/Lib/InnerBrowser.php index cf99680..ab1f473 100644 --- a/src/Codeception/Lib/InnerBrowser.php +++ b/src/Codeception/Lib/InnerBrowser.php @@ -1134,6 +1134,14 @@ protected function setCookiesFromOptions() } } + public function _after(TestInterface $test) + { + $this->client = null; + $this->crawler = null; + $this->forms = []; + $this->headers = []; + } + public function _failed(TestInterface $test, $fail) { try { @@ -1171,53 +1179,12 @@ public function _failed(TestInterface $test, $fail) $test->getMetadata()->addReport('response', $report); } - public function _after(TestInterface $test) - { - $this->client = null; - $this->crawler = null; - $this->forms = []; - $this->headers = []; - } - /** - * Send custom request to a backend using method, uri, parameters, etc. - * Use it in Helpers to create special request actions, like accessing API - * Returns a string with response body. - * - * ```php - * <?php - * // in Helper class - * public function createUserByApi($name) { - * $userData = $this->getModule('{{MODULE_NAME}}')->_request('POST', '/api/v1/users', ['name' => $name]); - * $user = json_decode($userData); - * return $user->id; - * } - * ?> - * ``` - * Does not load the response into the module so you can't interact with response page (click, fill forms). - * To load arbitrary page for interaction, use `_loadPage` method. - * - * @api - * @param $method - * @param $uri - * @param array $parameters - * @param array $files - * @param array $server - * @param null $content - * @return mixed|Crawler - * @throws ExternalUrlException - * @see `_loadPage` + * @return string */ - public function _request( - $method, - $uri, - array $parameters = [], - array $files = [], - array $server = [], - $content = null - ) { - $this->clientRequest($method, $uri, $parameters, $files, $server, $content, true); - return $this->_getResponseContent(); + public function _getCurrentUri() + { + return Uri::retrieveUri($this->getRunningClient()->getHistory()->current()->getUri()); } /** @@ -1236,13 +1203,20 @@ public function _request( * * @api * @return string - * @throws ModuleException */ public function _getResponseContent() { return (string)$this->getRunningClient()->getInternalResponse()->getContent(); } + /** + * @return int|string + */ + public function _getResponseStatusCode() + { + return $this->getResponseStatusCode(); + } + /** * Opens a page with arbitrary request parameters. * Useful for testing multi-step forms on a specific step. @@ -1278,48 +1252,55 @@ public function _loadPage( } /** - * Authenticates user for HTTP_AUTH - * - * @param $username - * @param $password - */ - public function amHttpAuthenticated($username, $password) - { - $this->client->setServerParameter('PHP_AUTH_USER', $username); - $this->client->setServerParameter('PHP_AUTH_PW', $password); - } - - /** - * Sets the HTTP header to the passed value - which is used on - * subsequent HTTP requests through PhpBrowser. + * Send custom request to a backend using method, uri, parameters, etc. + * Use it in Helpers to create special request actions, like accessing API + * Returns a string with response body. * - * Example: * ```php * <?php - * $I->haveHttpHeader('X-Requested-With', 'Codeception'); - * $I->amOnPage('test-headers.php'); + * // in Helper class + * public function createUserByApi($name) { + * $userData = $this->getModule('{{MODULE_NAME}}')->_request('POST', '/api/v1/users', ['name' => $name]); + * $user = json_decode($userData); + * return $user->id; + * } * ?> * ``` + * Does not load the response into the module so you can't interact with response page (click, fill forms). + * To load arbitrary page for interaction, use `_loadPage` method. * - * To use special chars in Header Key use HTML Character Entities: - * Example: - * Header with underscore - 'Client_Id' - * should be represented as - 'Client_Id' or 'Client_Id' - * - * ```php - * <?php - * $I->haveHttpHeader('Client_Id', 'Codeception'); - * ?> - * ``` + * @api + * @param $method + * @param $uri + * @param array $parameters + * @param array $files + * @param array $server + * @param null $content + * @return mixed|Crawler + * @see `_loadPage` + */ + public function _request( + $method, + $uri, + array $parameters = [], + array $files = [], + array $server = [], + $content = null + ) { + $this->clientRequest($method, $uri, $parameters, $files, $server, $content, true); + return $this->_getResponseContent(); + } + + /** + * Authenticates user for HTTP_AUTH * - * @param string $name the name of the request header - * @param string $value the value to set it to for subsequent - * requests + * @param $username + * @param $password */ - public function haveHttpHeader($name, $value) + public function amHttpAuthenticated($username, $password) { - $name = implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $name))))); - $this->headers[$name] = $value; + $this->client->setServerParameter('PHP_AUTH_USER', $username); + $this->client->setServerParameter('PHP_AUTH_PW', $password); } /** @@ -1346,75 +1327,106 @@ public function deleteHeader($name) } /** - * @return string - * @throws ModuleException - */ - public function _getCurrentUri() - { - return Uri::retrieveUri($this->getRunningClient()->getHistory()->current()->getUri()); - } - - /** - * Sends an ajax GET request with the passed parameters. - * See `sendAjaxPostRequest()` + * Checks that response code is equal to value provided. * - * @param $uri - * @param $params + * ```php + * <?php + * $I->dontSeeResponseCodeIs(200); + * + * // recommended \Codeception\Util\HttpCode + * $I->dontSeeResponseCodeIs(\Codeception\Util\HttpCode::OK); + * ``` + * + * @param $code */ - public function sendAjaxGetRequest($uri, $params = []) + public function dontSeeResponseCodeIs($code) { - $this->sendAjaxRequest('GET', $uri, $params); + $failureMessage = sprintf( + 'Expected HTTP status code other than %s', + HttpCode::getDescription($code) + ); + $this->assertNotEquals($code, $this->getResponseStatusCode(), $failureMessage); } /** - * Sends an ajax POST request with the passed parameters. - * The appropriate HTTP header is added automatically: - * `X-Requested-With: XMLHttpRequest` + * Sets the HTTP header to the passed value - which is used on + * subsequent HTTP requests through PhpBrowser. + * * Example: - * ``` php + * ```php * <?php - * $I->sendAjaxPostRequest('/add-task', ['task' => 'lorem ipsum']); + * $I->haveHttpHeader('X-Requested-With', 'Codeception'); + * $I->amOnPage('test-headers.php'); + * ?> * ``` - * Some frameworks (e.g. Symfony) create field names in the form of an "array": - * `<input type="text" name="form[task]">` - * In this case you need to pass the fields like this: - * ``` php + * + * To use special chars in Header Key use HTML Character Entities: + * Example: + * Header with underscore - 'Client_Id' + * should be represented as - 'Client_Id' or 'Client_Id' + * + * ```php * <?php - * $I->sendAjaxPostRequest('/add-task', ['form' => [ - * 'task' => 'lorem ipsum', - * 'category' => 'miscellaneous', - * ]]); - * ``` + * $I->haveHttpHeader('Client_Id', 'Codeception'); + * ?> + * ``` * - * @param string $uri - * @param array $params + * @param string $name the name of the request header + * @param string $value the value to set it to for subsequent + * requests */ - public function sendAjaxPostRequest($uri, $params = []) + public function haveHttpHeader($name, $value) { - $this->sendAjaxRequest('POST', $uri, $params); + $name = implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $name))))); + $this->headers[$name] = $value; } /** - * Sends an ajax request, using the passed HTTP method. - * See `sendAjaxPostRequest()` - * Example: - * ``` php - * <?php - * $I->sendAjaxRequest('PUT', '/posts/7', ['title' => 'new title']); - * ``` + * Sets SERVER parameter valid for all next requests. * - * @param $method - * @param $uri - * @param $params + * ```php + * $I->haveServerParameter('name', 'value'); + * ``` + * @param $name + * @param $value */ - public function sendAjaxRequest($method, $uri, $params = []) + public function haveServerParameter($name, $value) { - $this->clientRequest($method, $uri, $params, [], ['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'], null, false); + $this->client->setServerParameter($name, $value); } - public function _getResponseStatusCode() + /** + * Moves back in history. + * + * @param int $numberOfSteps (default value 1) + */ + public function moveBack($numberOfSteps = 1) { - return $this->getResponseStatusCode(); + if (!is_int($numberOfSteps) || $numberOfSteps < 1) { + throw new InvalidArgumentException('numberOfSteps must be positive integer'); + } + try { + $history = $this->getRunningClient()->getHistory(); + for ($i = $numberOfSteps; $i > 0; $i--) { + $request = $history->back(); + } + } catch (LogicException $e) { + throw new InvalidArgumentException( + sprintf( + 'numberOfSteps is set to %d, but there are only %d previous steps in the history', + $numberOfSteps, + $numberOfSteps - $i + ) + ); + } + $this->_loadPage( + $request->getMethod(), + $request->getUri(), + $request->getParameters(), + $request->getFiles(), + $request->getServer(), + $request->getContent() + ); } /** @@ -1467,121 +1479,97 @@ public function seeResponseCodeIsBetween($from, $to) } /** - * Checks that response code is equal to value provided. - * - * ```php - * <?php - * $I->dontSeeResponseCodeIs(200); - * - * // recommended \Codeception\Util\HttpCode - * $I->dontSeeResponseCodeIs(\Codeception\Util\HttpCode::OK); - * ``` - * @param $code + * Checks that the response code is 4xx */ - public function dontSeeResponseCodeIs($code) + public function seeResponseCodeIsClientError() { - $failureMessage = sprintf( - 'Expected HTTP status code other than %s', - HttpCode::getDescription($code) - ); - $this->assertNotEquals($code, $this->getResponseStatusCode(), $failureMessage); + $this->seeResponseCodeIsBetween(400, 499); } /** - * Checks that the response code 2xx + * Checks that the response code 3xx */ - public function seeResponseCodeIsSuccessful() + public function seeResponseCodeIsRedirection() { - $this->seeResponseCodeIsBetween(200, 299); + $this->seeResponseCodeIsBetween(300, 399); } /** - * Checks that the response code 3xx + * Checks that the response code is 5xx */ - public function seeResponseCodeIsRedirection() + public function seeResponseCodeIsServerError() { - $this->seeResponseCodeIsBetween(300, 399); + $this->seeResponseCodeIsBetween(500, 599); } /** - * Checks that the response code is 4xx + * Checks that the response code 2xx */ - public function seeResponseCodeIsClientError() + public function seeResponseCodeIsSuccessful() { - $this->seeResponseCodeIsBetween(400, 499); + $this->seeResponseCodeIsBetween(200, 299); } /** - * Checks that the response code is 5xx + * If your page triggers an ajax request, you can perform it manually. + * This action sends a GET ajax request with specified params. + * + * See ->sendAjaxPostRequest for examples. + * + * @param $uri + * @param array $params */ - public function seeResponseCodeIsServerError() + public function sendAjaxGetRequest($uri, $params = []) { - $this->seeResponseCodeIsBetween(500, 599); + $this->sendAjaxRequest('GET', $uri, $params); } /** - * Switch to iframe or frame on the page. + * If your page triggers an ajax request, you can perform it manually. + * This action sends a POST ajax request with specified params. + * Additional params can be passed as array. * * Example: - * ``` html - * <iframe name="another_frame" src="http://example.com"> - * ``` + * + * Imagine that by clicking checkbox you trigger ajax request which updates user settings. + * We emulate that click by running this ajax request manually. * * ``` php * <?php - * # switch to iframe - * $I->switchToIframe("another_frame"); + * $I->sendAjaxPostRequest('/updateSettings', array('notifications' => true)); // POST + * $I->sendAjaxGetRequest('/updateSettings', array('notifications' => true)); // GET + * * ``` * - * @param string $name + * @param $uri + * @param array $params */ - - public function switchToIframe($name) + public function sendAjaxPostRequest($uri, $params = []) { - $iframe = $this->match("iframe[name=$name]")->first(); - if (!count($iframe)) { - $iframe = $this->match("frame[name=$name]")->first(); - } - if (!count($iframe)) { - throw new ElementNotFound("name=$name", 'Iframe'); - } - - $uri = $iframe->getNode(0)->getAttribute('src'); - $this->amOnPage($uri); + $this->sendAjaxRequest('POST', $uri, $params); } /** - * Moves back in history. + * If your page triggers an ajax request, you can perform it manually. + * This action sends an ajax request with specified method and params. * - * @param int $numberOfSteps (default value 1) + * Example: + * + * You need to perform an ajax request specifying the HTTP method. + * + * ``` php + * <?php + * $I->sendAjaxRequest('PUT', '/posts/7', array('title' => 'new title')); + * + * ``` + * + * @param $method + * @param $uri + * @param array $params */ - public function moveBack($numberOfSteps = 1) + public function sendAjaxRequest($method, $uri, $params = []) { - if (!is_int($numberOfSteps) || $numberOfSteps < 1) { - throw new InvalidArgumentException('numberOfSteps must be positive integer'); - } - try { - $history = $this->getRunningClient()->getHistory(); - for ($i = $numberOfSteps; $i > 0; $i--) { - $request = $history->back(); - } - } catch (LogicException $e) { - throw new InvalidArgumentException( - sprintf( - 'numberOfSteps is set to %d, but there are only %d previous steps in the history', - $numberOfSteps, - $numberOfSteps - $i - ) - ); - } - $this->_loadPage( - $request->getMethod(), - $request->getUri(), - $request->getParameters(), - $request->getFiles(), - $request->getServer(), - $request->getContent() - ); + $this->clientRequest($method, $uri, $params, [], ['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'], null, false); } /** @@ -1599,16 +1587,32 @@ public function setServerParameters(array $params) } /** - * Sets SERVER parameter valid for all next requests. + * Switch to iframe or frame on the page. * - * ```php - * $I->haveServerParameter('name', 'value'); + * Example: + * ``` html + * <iframe name="another_frame" src="http://example.com"> + * ``` + * + * ``` php + * <?php + * # switch to iframe + * $I->switchToIframe("another_frame"); * ``` + * * @param $name - * @param $value */ - public function haveServerParameter($name, $value) + public function switchToIframe($name) { - $this->client->setServerParameter($name, $value); + $iframe = $this->match("iframe[name=$name]")->first(); + if (!count($iframe)) { + $iframe = $this->match("frame[name=$name]")->first(); + } + if (!count($iframe)) { + throw new ElementNotFound("name=$name", 'Iframe'); + } + + $uri = $iframe->getNode(0)->getAttribute('src'); + $this->amOnPage($uri); } } From 2783718470c9e7f475251221ec101e21c322f539 Mon Sep 17 00:00:00 2001 From: Tavo Nieves J <ganieves@outlook.com> Date: Sun, 11 Oct 2020 19:49:27 -0500 Subject: [PATCH 6/7] Fix code style --- src/Codeception/Lib/InnerBrowser.php | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Codeception/Lib/InnerBrowser.php b/src/Codeception/Lib/InnerBrowser.php index ab1f473..b0c77cc 100644 --- a/src/Codeception/Lib/InnerBrowser.php +++ b/src/Codeception/Lib/InnerBrowser.php @@ -1,4 +1,5 @@ <?php + namespace Codeception\Lib; use Codeception\Exception\ElementNotFound; @@ -45,7 +46,7 @@ class_alias('Symfony\Component\BrowserKit\Client', 'Symfony\Component\BrowserKit\AbstractBrowser'); } -class InnerBrowser extends Module implements Web, PageSourceSaver, ElementLocator, ConflictsWithModule +class InnerBrowser extends Module implements ConflictsWithModule, ElementLocator, PageSourceSaver, Web { use ConflictsWithModuleTrait, @@ -87,7 +88,7 @@ class InnerBrowser extends Module implements Web, PageSourceSaver, ElementLocato * Clicks the link or submits the form when the button is clicked * @param DOMNode $node * @return boolean clicked something - * @throws ModuleException + * @throws ModuleException|ExternalUrlException */ private function clickButton(DOMNode $node) { @@ -173,6 +174,7 @@ private function getCrawler() * * @param Crawler $form the form * @return Form + * @throws ModuleException */ private function getFormFromCrawler(Crawler $form) { @@ -245,6 +247,17 @@ private function stringifySelector($selector) return $selector; } + /** + * @param $method + * @param $uri + * @param array $parameters + * @param array $files + * @param array $server + * @param null $content + * @param bool $changeHistory + * @return mixed|Crawler|null + * @throws ExternalUrlException|ModuleException + */ protected function clientRequest( $method, $uri, @@ -325,7 +338,6 @@ protected function assertDomNotContains($nodes, $message, $text = '') /** * @param $needle * @param string $message - * @throws ModuleException */ protected function assertPageContains($needle, $message = '') { @@ -340,7 +352,6 @@ protected function assertPageContains($needle, $message = '') /** * @param $needle * @param string $message - * @throws ModuleException */ protected function assertPageNotContains($needle, $message = '') { @@ -375,7 +386,7 @@ protected function assertPageSourceNotContains($needle, $message = '') /** * @param $link * @return bool - * @throws ModuleException + * @throws ModuleException|ExternalUrlException */ protected function clickByLocator($link) { @@ -535,6 +546,7 @@ protected function getFieldsByLabelOrCss($field) * * @param Crawler $node * @return Form + * @throws ModuleException */ protected function getFormFor(Crawler $node) { @@ -646,7 +658,6 @@ protected function getInputValue($input) /** * @return string - * @throws ModuleException */ protected function getNormalizedResponseContent() { From 32954576c0799b7642324cdc556118933c7d6c15 Mon Sep 17 00:00:00 2001 From: Tavo Nieves J <ganieves@outlook.com> Date: Sun, 11 Oct 2020 20:01:51 -0500 Subject: [PATCH 7/7] added BrowserInterface --- src/Codeception/Lib/InnerBrowser.php | 7 +- .../Lib/Model/BrowserInterface.php | 76 +++++++++++++++++++ 2 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 src/Codeception/Lib/Model/BrowserInterface.php diff --git a/src/Codeception/Lib/InnerBrowser.php b/src/Codeception/Lib/InnerBrowser.php index b0c77cc..502a133 100644 --- a/src/Codeception/Lib/InnerBrowser.php +++ b/src/Codeception/Lib/InnerBrowser.php @@ -7,10 +7,7 @@ use Codeception\Exception\MalformedLocatorException; use Codeception\Exception\ModuleException; use Codeception\Exception\TestRuntimeException; -use Codeception\Lib\Interfaces\ConflictsWithModule; -use Codeception\Lib\Interfaces\ElementLocator; -use Codeception\Lib\Interfaces\PageSourceSaver; -use Codeception\Lib\Interfaces\Web; +use Codeception\Lib\Model\BrowserInterface; use Codeception\Lib\Model\ConflictsWithModuleTrait; use Codeception\Lib\Model\ElementLocatorTrait; use Codeception\Lib\Model\PageSourceSaverTrait; @@ -46,7 +43,7 @@ class_alias('Symfony\Component\BrowserKit\Client', 'Symfony\Component\BrowserKit\AbstractBrowser'); } -class InnerBrowser extends Module implements ConflictsWithModule, ElementLocator, PageSourceSaver, Web +class InnerBrowser extends Module implements BrowserInterface { use ConflictsWithModuleTrait, diff --git a/src/Codeception/Lib/Model/BrowserInterface.php b/src/Codeception/Lib/Model/BrowserInterface.php new file mode 100644 index 0000000..66cff5e --- /dev/null +++ b/src/Codeception/Lib/Model/BrowserInterface.php @@ -0,0 +1,76 @@ +<?php + +namespace Codeception\Lib\Model; + +use Codeception\Lib\Interfaces\ConflictsWithModule; +use Codeception\Lib\Interfaces\ElementLocator; +use Codeception\Lib\Interfaces\PageSourceSaver; +use Codeception\Lib\Interfaces\Web; +use Codeception\TestInterface; + +interface BrowserInterface extends ConflictsWithModule, ElementLocator, PageSourceSaver, Web +{ + public function _after(TestInterface $test); + + public function _failed(TestInterface $test, $fail); + + public function _getCurrentUri(); + + public function _getResponseContent(); + + public function _getResponseStatusCode(); + + public function _loadPage( + $method, + $uri, + array $parameters = [], + array $files = [], + array $server = [], + $content = null + ); + + public function _request( + $method, + $uri, + array $parameters = [], + array $files = [], + array $server = [], + $content = null + ); + + public function amHttpAuthenticated($username, $password); + + public function deleteHeader($name); + + public function dontSeeResponseCodeIs($code); + + public function haveHttpHeader($name, $value); + + public function haveServerParameter($name, $value); + + public function moveBack($numberOfSteps = 1); + + public function seePageNotFound(); + + public function seeResponseCodeIs($code); + + public function seeResponseCodeIsBetween($from, $to); + + public function seeResponseCodeIsClientError(); + + public function seeResponseCodeIsRedirection(); + + public function seeResponseCodeIsServerError(); + + public function seeResponseCodeIsSuccessful(); + + public function sendAjaxGetRequest($uri, $params = []); + + public function sendAjaxPostRequest($uri, $params = []); + + public function sendAjaxRequest($method, $uri, $params = []); + + public function setServerParameters(array $params); + + public function switchToIframe($name); +} \ No newline at end of file