diff --git a/README.md b/README.md index c93f861d..86fbf620 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,22 @@ This will start a Firefox browser by default. Other browsers and profiles can be For example, if you want to start a Chrome Browser you can following the instructions provided [here](docs/chrome-behat.md). +### Running with stand-alone command + +If running with `silverstripe/serve` and `chromedriver`, you can also use the following command +which will automatically start and stop these services for individual tests. + + vendor/bin/behat-ss @framework + +This automates: + - starting server + - starting chromedriver + - running behat + - shutting down chromedriver + - shutting down server + +Make sure you set `SS_BASE_URL` to `http://localhost:8080` in `.env` + ## Tutorials * [Tutorial: Testing Form Submissions](docs/tutorial.md) diff --git a/bin/behat-ss b/bin/behat-ss new file mode 100755 index 00000000..0bbfc3f3 --- /dev/null +++ b/bin/behat-ss @@ -0,0 +1,20 @@ +#!/bin/sh +echo "setting up /artifacts" +mkdir -p artifacts + +echo "starting chromedriver" +chromedriver &> artifacts/chromedriver.log 2> artifacts/chromedriver-error.log & +cd_pid=$! + +echo "starting webserver" +vendor/bin/serve &> artifacts/serve.log 2> artifacts/serve-error.log & +ws_pid=$! + +echo "starting behat" +vendor/bin/behat "$@" + +echo "killing webserver (PID: $ws_pid)" +pkill -TERM -P $ws_pid &> /dev/null + +echo "killing chromedriver (PID: $cd_pid)" +kill -9 $cd_pid &> /dev/null diff --git a/composer.json b/composer.json index 47174de2..de89ecd0 100644 --- a/composer.json +++ b/composer.json @@ -26,10 +26,10 @@ "behat/behat": "^3.2", "behat/mink": "^1.7", "behat/mink-extension": "^2.1", - "behat/mink-selenium2-driver": "^1.3", + "silverstripe/mink-facebook-web-driver": "^1", "symfony/dom-crawler": "^3", - "silverstripe/testsession": "^2.0.0@alpha", - "silverstripe/framework": "^4@dev", + "silverstripe/testsession": "^2.1", + "silverstripe/framework": "^4", "symfony/finder": "^3.2" }, "autoload": { @@ -47,6 +47,9 @@ "3.x-dev": "3.1.x-dev" } }, + "bin": [ + "bin/behat-ss" + ], "scripts": { "lint": "phpcs --standard=PSR2 -n src/ tests/php/" }, diff --git a/src/Context/BasicContext.php b/src/Context/BasicContext.php index a861b32d..e4cb4166 100644 --- a/src/Context/BasicContext.php +++ b/src/Context/BasicContext.php @@ -8,16 +8,19 @@ use Behat\Behat\Hook\Scope\AfterStepScope; use Behat\Behat\Hook\Scope\BeforeStepScope; use Behat\Behat\Hook\Scope\StepScope; -use Behat\Mink\Driver\Selenium2Driver; use Behat\Mink\Element\NodeElement; use Behat\Mink\Session; use Behat\Testwork\Tester\Result\TestResult; use Exception; +use Facebook\WebDriver\Exception\WebDriverException; +use Facebook\WebDriver\WebDriver; +use Facebook\WebDriver\WebDriverAlert; +use Facebook\WebDriver\WebDriverExpectedCondition; +use InvalidArgumentException; use SilverStripe\Assets\File; use SilverStripe\Assets\Filesystem; use SilverStripe\BehatExtension\Utility\StepHelper; -use WebDriver\Exception as WebDriverException; -use WebDriver\Session as WebDriverSession; +use SilverStripe\MinkFacebookWebDriver\FacebookWebDriver; /** * BasicContext @@ -248,7 +251,7 @@ public function handleAjaxTimeout() /** * Take screenshot when step fails. - * Works only with Selenium2Driver. + * Works only with FacebookWebDriver. * * @AfterStep * @param AfterStepScope $event @@ -282,7 +285,7 @@ public function closeModalDialog(AfterScenarioScope $event) try { // Navigate away triggered by reloading the page $this->getSession()->reload(); - $this->getWebDriverSession()->accept_alert(); + $this->getExpectedAlert()->accept(); } catch (WebDriverException $e) { // no-op, alert might not be present } @@ -316,8 +319,8 @@ public function takeScreenshot(StepScope $event) { // Validate driver $driver = $this->getSession()->getDriver(); - if (!($driver instanceof Selenium2Driver)) { - file_put_contents('php://stdout', 'ScreenShots are only supported for Selenium2Driver: skipping'); + if (!($driver instanceof FacebookWebDriver)) { + file_put_contents('php://stdout', 'ScreenShots are only supported for FacebookWebDriver: skipping'); return; } @@ -350,8 +353,8 @@ public function takeScreenshot(StepScope $event) } $path = sprintf('%s/%s_%d.png', $path, basename($feature->getFile()), $step->getLine()); - $screenshot = $driver->getWebDriverSession()->screenshot(); - file_put_contents($path, base64_decode($screenshot)); + $screenshot = $driver->getScreenshot(); + file_put_contents($path, $screenshot); file_put_contents('php://stderr', sprintf('Saving screenshot into %s' . PHP_EOL, $path)); } @@ -521,10 +524,7 @@ public function iClickInTheElementDismissingTheDialog($clickType, $text, $select */ public function iSeeTheDialogText($expected) { - $session = $this->getSession(); - /** @var Selenium2Driver $driver */ - $driver = $session->getDriver(); - $text = $driver->getWebDriverSession()->getAlert_text(); + $text = $this->getExpectedAlert()->getText(); assertContains($expected, $text); } @@ -534,7 +534,24 @@ public function iSeeTheDialogText($expected) */ public function iTypeIntoTheDialog($data) { - $this->getWebDriverSession()->postAlert_text([ 'text' => $data ]); + $this->getExpectedAlert() + ->sendKeys($data) + ->accept(); + } + + /** + * Wait for alert to appear, and return handle + * + * @return WebDriverAlert + */ + protected function getExpectedAlert() + { + $session = $this->getWebDriverSession(); + $session->wait()->until( + WebDriverExpectedCondition::alertIsPresent(), + "Alert is expected" + ); + return $session->switchTo()->alert(); } /** @@ -542,7 +559,12 @@ public function iTypeIntoTheDialog($data) */ public function iConfirmTheDialog() { - $this->getWebDriverSession()->accept_alert(); + $session = $this->getWebDriverSession(); + $session->wait()->until( + WebDriverExpectedCondition::alertIsPresent(), + "Alert is expected" + ); + $session->switchTo()->alert()->accept(); $this->handleAjaxTimeout(); } @@ -551,23 +573,23 @@ public function iConfirmTheDialog() */ public function iDismissTheDialog() { - $this->getWebDriverSession()->dismiss_alert(); + $this->getExpectedAlert()->dismiss(); $this->handleAjaxTimeout(); } /** * Get Selenium webdriver session. - * Note: Will fail if current driver isn't Selenium2Driver + * Note: Will fail if current driver isn't FacebookWebDriver * - * @return WebDriverSession + * @return WebDriver */ protected function getWebDriverSession() { $driver = $this->getSession()->getDriver(); - if (! $driver instanceof Selenium2Driver) { - throw new \InvalidArgumentException("Not supported for non-selenium2 drivers"); + if (! $driver instanceof FacebookWebDriver) { + throw new InvalidArgumentException("Only supported for FacebookWebDriver"); } - return $driver->getWebDriverSession(); + return $driver->getWebDriver(); } /** @@ -612,7 +634,7 @@ public function iSelectFromInputGroup($value, $labelText) } if (!$parent) { - throw new \InvalidArgumentException(sprintf('Input group with label "%s" cannot be found', $labelText)); + throw new InvalidArgumentException(sprintf('Input group with label "%s" cannot be found', $labelText)); } /** @var NodeElement $option */ @@ -632,7 +654,7 @@ public function iSelectFromInputGroup($value, $labelText) } if (!$input) { - throw new \InvalidArgumentException(sprintf('Input "%s" cannot be found', $value)); + throw new InvalidArgumentException(sprintf('Input "%s" cannot be found', $value)); } $this->getSession()->getDriver()->click($input->getXPath()); @@ -649,6 +671,7 @@ public function iPutABreakpoint() { fwrite(STDOUT, "\033[s \033[93m[Breakpoint] Press \033[1;93m[RETURN]\033[0;93m to continue...\033[0m"); while (fgets(STDIN, 1024) == '') { + // noop } fwrite(STDOUT, "\033[u"); @@ -669,7 +692,7 @@ public function castRelativeToAbsoluteTime($prefix, $val) { $timestamp = strtotime($val); if (!$timestamp) { - throw new \InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( "Can't resolve '%s' into a valid datetime value", $val )); @@ -691,7 +714,7 @@ public function castRelativeToAbsoluteDatetime($prefix, $val) { $timestamp = strtotime($val); if (!$timestamp) { - throw new \InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( "Can't resolve '%s' into a valid datetime value", $val )); @@ -713,7 +736,7 @@ public function castRelativeToAbsoluteDate($prefix, $val) { $timestamp = strtotime($val); if (!$timestamp) { - throw new \InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( "Can't resolve '%s' into a valid datetime value", $val )); @@ -1150,7 +1173,7 @@ public function iScrollToField($locator, $type) $id = $el->getAttribute('id'); if (empty($id)) { - throw new \InvalidArgumentException('Element requires an "id" attribute'); + throw new InvalidArgumentException('Element requires an "id" attribute'); } $js = sprintf("document.getElementById('%s').scrollIntoView(true);", $id); @@ -1173,7 +1196,7 @@ public function iScrollToElement($locator) $id = $el->getAttribute('id'); if (empty($id)) { - throw new \InvalidArgumentException('Element requires an "id" attribute'); + throw new InvalidArgumentException('Element requires an "id" attribute'); } $js = sprintf("document.getElementById('%s').scrollIntoView(true);", $id); diff --git a/src/Context/LoginContext.php b/src/Context/LoginContext.php index 8d1f6a79..a7007ea0 100644 --- a/src/Context/LoginContext.php +++ b/src/Context/LoginContext.php @@ -109,6 +109,17 @@ public function stepILogInWith($email, $password) $emailField->setValue($email); $passwordField->setValue($password); $submitButton->press(); + + // Wait 100 ms + $this->getMainContext()->getSession()->wait(100); + + // In case of login error, throw exception + // E.g. 'Your session has expired. Please re-submit the form.' + // This will allow @retry + $page = $this->getMainContext()->getSession()->getPage(); + $message = $page->find('css', '.message.error'); + $error = $message ? $message->getText() : null; + assertNull($message, 'Could not log in with user ' . $email . '. Error: "' . $error. '""'); } /** diff --git a/src/Context/SilverStripeContext.php b/src/Context/SilverStripeContext.php index 64349069..21bf0eed 100644 --- a/src/Context/SilverStripeContext.php +++ b/src/Context/SilverStripeContext.php @@ -4,17 +4,18 @@ use Behat\Behat\Hook\Scope\BeforeScenarioScope; use Behat\Mink\Element\NodeElement; +use Behat\Mink\Exception\ElementNotFoundException; +use Behat\Mink\Exception\UnsupportedDriverActionException; use Behat\Mink\Selector\Xpath\Escaper; use Behat\MinkExtension\Context\MinkContext; -use Behat\Mink\Driver\Selenium2Driver; -use Behat\Mink\Exception\UnsupportedDriverActionException; -use Behat\Mink\Exception\ElementNotFoundException; use InvalidArgumentException; use SilverStripe\BehatExtension\Utility\TestMailer; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Core\ClassInfo; +use SilverStripe\Core\Convert; use SilverStripe\Core\Environment; use SilverStripe\Core\Resettable; +use SilverStripe\MinkFacebookWebDriver\FacebookWebDriver; use SilverStripe\ORM\DataObject; use SilverStripe\TestSession\TestSessionEnvironment; use Symfony\Component\CssSelector\Exception\SyntaxErrorException; @@ -383,7 +384,7 @@ public function joinUrlParts($part = null) public function canIntercept() { $driver = $this->getSession()->getDriver(); - if ($driver instanceof Selenium2Driver) { + if ($driver instanceof FacebookWebDriver) { return false; } @@ -411,7 +412,18 @@ public function fillField($field, $value) /** @var NodeElement $node */ foreach ($nodes as $node) { if ($node->isVisible()) { - $node->setValue($value); + // Work around for https://github.com/FluentLenium/FluentLenium/issues/129 + // Otherwise "Element must be user-editable in order to clear it" + $type = $node->getAttribute('type'); + $id = $node->getAttribute('id'); + if ($type === 'date' && $id) { + $jsValue = Convert::raw2js($value); + $this->getSession()->getDriver()->executeScript( + "document.getElementById(\"{$id}\").value = \"{$jsValue}\";" + ); + } else { + $node->setValue($value); + } return; } } diff --git a/src/MinkExtension.php b/src/MinkExtension.php index c042f9c4..2bd46417 100644 --- a/src/MinkExtension.php +++ b/src/MinkExtension.php @@ -4,6 +4,7 @@ use Behat\MinkExtension\ServiceContainer\MinkExtension as BaseMinkExtension; use SilverStripe\BehatExtension\Compiler\MinkExtensionBaseUrlPass; +use SilverStripe\MinkFacebookWebDriver\FacebookFactory; use Symfony\Component\DependencyInjection\ContainerBuilder; /** @@ -14,6 +15,12 @@ */ class MinkExtension extends BaseMinkExtension { + public function __construct() + { + parent::__construct(); + $this->registerDriverFactory(new FacebookFactory()); + } + public function process(ContainerBuilder $container) { parent::process($container);