mirror of
https://github.com/silverstripe/silverstripe-behat-extension
synced 2024-10-22 17:05:32 +02:00
NEW Skip MFA on login if present (#204)
This commit is contained in:
parent
0939a30b12
commit
bc76e19d5d
@ -2,25 +2,29 @@
|
||||
|
||||
namespace SilverStripe\BehatExtension\Context;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Behat\Behat\Context\Context;
|
||||
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
|
||||
use Behat\Behat\Definition\Call;
|
||||
use Behat\Behat\Hook\Scope\AfterScenarioScope;
|
||||
use Behat\Behat\Hook\Scope\AfterStepScope;
|
||||
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
|
||||
use Behat\Behat\Hook\Scope\BeforeStepScope;
|
||||
use Behat\Behat\Hook\Scope\StepScope;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Behat\Mink\Exception\ElementNotFoundException;
|
||||
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 Facebook\WebDriver\WebDriverKeys;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use SilverStripe\Assets\File;
|
||||
use SilverStripe\Assets\Filesystem;
|
||||
use SilverStripe\BehatExtension\Utility\StepHelper;
|
||||
use SilverStripe\BehatExtension\Utility\DebugTools;
|
||||
use SilverStripe\MinkFacebookWebDriver\FacebookWebDriver;
|
||||
|
||||
/**
|
||||
@ -35,6 +39,7 @@ class BasicContext implements Context
|
||||
{
|
||||
use MainContextAwareTrait;
|
||||
use StepHelper;
|
||||
use DebugTools;
|
||||
|
||||
/**
|
||||
* Date format in date() syntax
|
||||
@ -55,6 +60,50 @@ class BasicContext implements Context
|
||||
*/
|
||||
protected $datetimeFormat = 'Y-m-d H:i:s';
|
||||
|
||||
/**
|
||||
* @var FixtureContext
|
||||
*/
|
||||
protected $fixtureContext = null;
|
||||
|
||||
/**
|
||||
* Get the fixture context of the current module
|
||||
*
|
||||
* @BeforeScenario
|
||||
*/
|
||||
public function gatherContexts(BeforeScenarioScope $scope): void
|
||||
{
|
||||
/** @var InitializedContextEnvironment $environment */
|
||||
$environment = $scope->getEnvironment();
|
||||
|
||||
// Find the FixtureContext defined in behat.yml
|
||||
$subClasses = $this->getSubclassesOf(FixtureContext::class);
|
||||
foreach ($subClasses as $class) {
|
||||
if (!$environment->hasContextClass($class)) {
|
||||
continue;
|
||||
}
|
||||
$this->fixtureContext = $environment->getContext($class);
|
||||
break;
|
||||
}
|
||||
// Fallback to base FixtureClass
|
||||
if (!$this->fixtureContext && $environment->hasContextClass(FixtureContext::class)) {
|
||||
$this->fixtureContext = $environment->getContext(FixtureContext::class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the subclasses of a class
|
||||
*/
|
||||
private function getSubclassesOf($parent): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (get_declared_classes() as $class) {
|
||||
if (is_subclass_of($class, $parent)) {
|
||||
$result[] = $class;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Mink session from MinkContext
|
||||
*
|
||||
@ -129,7 +178,7 @@ JS;
|
||||
$jserrors = $page->find('xpath', '//body[@data-jserrors]');
|
||||
if (null !== $jserrors) {
|
||||
$this->takeScreenshot($event);
|
||||
file_put_contents('php://stderr', $jserrors->getAttribute('data-jserrors') . PHP_EOL);
|
||||
$this->logMessage($jserrors->getAttribute('data-jserrors'));
|
||||
}
|
||||
|
||||
$javascript = <<<JS
|
||||
@ -250,26 +299,6 @@ JS;
|
||||
$this->getSession()->wait(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take screenshot when step fails.
|
||||
* Works only with FacebookWebDriver.
|
||||
*
|
||||
* @AfterStep
|
||||
* @param AfterStepScope $event
|
||||
*/
|
||||
public function takeScreenshotAfterFailedStep(AfterStepScope $event)
|
||||
{
|
||||
// Check failure code
|
||||
if ($event->getTestResult()->getResultCode() !== TestResult::FAILED) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->takeScreenshot($event);
|
||||
} catch (WebDriverException $e) {
|
||||
$this->logException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal dialog if test scenario fails on CMS page
|
||||
*
|
||||
@ -313,54 +342,6 @@ JS;
|
||||
Filesystem::removeFolder(ASSETS_PATH, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a nice screenshot
|
||||
*
|
||||
* @param StepScope $event
|
||||
*/
|
||||
public function takeScreenshot(StepScope $event)
|
||||
{
|
||||
// Validate driver
|
||||
$driver = $this->getSession()->getDriver();
|
||||
if (!($driver instanceof FacebookWebDriver)) {
|
||||
file_put_contents('php://stdout', 'ScreenShots are only supported for FacebookWebDriver: skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
$feature = $event->getFeature();
|
||||
$step = $event->getStep();
|
||||
$screenshotPath = null;
|
||||
|
||||
// Check paths are configured
|
||||
$path = $this->getMainContext()->getScreenshotPath();
|
||||
if (!$path) {
|
||||
file_put_contents('php://stdout', 'ScreenShots path not configured: skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
Filesystem::makeFolder($path);
|
||||
$path = realpath($path);
|
||||
|
||||
if (!file_exists($path)) {
|
||||
file_put_contents('php://stderr', sprintf('"%s" is not valid directory and failed to create it' . PHP_EOL, $path));
|
||||
return;
|
||||
}
|
||||
|
||||
if (file_exists($path) && !is_dir($path)) {
|
||||
file_put_contents('php://stderr', sprintf('"%s" is not valid directory' . PHP_EOL, $path));
|
||||
return;
|
||||
}
|
||||
if (file_exists($path) && !is_writable($path)) {
|
||||
file_put_contents('php://stderr', sprintf('"%s" directory is not writable' . PHP_EOL, $path));
|
||||
return;
|
||||
}
|
||||
|
||||
$path = sprintf('%s/%s_%d.png', $path, basename($feature->getFile()), $step->getLine());
|
||||
$screenshot = $driver->getScreenshot();
|
||||
file_put_contents($path, $screenshot);
|
||||
file_put_contents('php://stderr', sprintf('Saving screenshot into %s' . PHP_EOL, $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^the page can't be found/
|
||||
*/
|
||||
@ -642,6 +623,8 @@ JS;
|
||||
* @param string $field
|
||||
* @param string $path
|
||||
* @return Call\Given
|
||||
*
|
||||
* @deprecated 4.5..5.0 - use iAttachTheFileToTheField() instead
|
||||
*/
|
||||
public function iAttachTheFileTo($field, $path)
|
||||
{
|
||||
@ -925,7 +908,6 @@ JS;
|
||||
$regionObj->fillField($field, $value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asserts text in a specific region (an element identified by a CSS selector, a "data-title" attribute,
|
||||
* or a named region mapped to a CSS selector via Behat configuration).
|
||||
@ -1281,6 +1263,16 @@ JS;
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log a message
|
||||
*/
|
||||
protected function logMessage(string $message)
|
||||
{
|
||||
file_put_contents('php://stderr', $message . PHP_EOL);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* We have to catch exceptions and log somehow else otherwise behat falls over
|
||||
*
|
||||
@ -1288,7 +1280,7 @@ JS;
|
||||
*/
|
||||
protected function logException(Exception $exception)
|
||||
{
|
||||
file_put_contents('php://stderr', 'Exception caught: ' . $exception->getMessage());
|
||||
$this->logMessage('Exception caught: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1296,32 +1288,248 @@ JS;
|
||||
* There's already an xpath based function 'I see the "" element' iSeeTheElement() in silverstripe/cms
|
||||
* There's also an 'I should see "" element' in MinkContext which also converts the css selector to xpath
|
||||
*
|
||||
* @When /^I should see the "([^"]+)" element/
|
||||
* @When /^I should(| not) see the "([^"]+)" element/
|
||||
* @param $selector
|
||||
*/
|
||||
public function iShouldSeeTheElement($selector)
|
||||
public function iShouldSeeTheElement($not, $cssSelector = '')
|
||||
{
|
||||
$sel = str_replace('"', '\\"', $selector);
|
||||
// backwards compatibility for when function signature was just ($cssSelector)
|
||||
if (!in_array($not, ['', ' not'])) {
|
||||
$not = '';
|
||||
$cssSelector = $not;
|
||||
}
|
||||
$sel = str_replace('"', '\\"', $cssSelector);
|
||||
$js = <<<JS
|
||||
return document.querySelector("$sel");
|
||||
JS;
|
||||
$element = $this->getSession()->evaluateScript($js);
|
||||
Assert::assertNotNull($element, sprintf('Element %s not found', $selector));
|
||||
if ($not) {
|
||||
Assert::assertNull($element, sprintf('Element %s was found when it should not have been', $cssSelector));
|
||||
} else {
|
||||
Assert::assertNotNull($element, sprintf('Element %s not found', $cssSelector));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the option in select field with specified id|name|label|value.
|
||||
* Note: this is duplicate code from SilverStripeContext selectOption
|
||||
* In practice, the primary context file using in modules have inherited from BasicContext
|
||||
* and not SilverStripeContext so the selectOption method is not available.
|
||||
* Selects the option in select field with specified id|name|label|value
|
||||
* Also accepts CSS selectors
|
||||
*
|
||||
* @When /^I select "([^"]+)" from the "([^"]+)" field$/
|
||||
* @When /^I select "([^"]+)" from the "([^"]+)" field(| with javascript)$/
|
||||
* @param string $value
|
||||
* @param string $locator - select id, name or label - NOT a css selector
|
||||
* @param string $locator - select id, name, label or element
|
||||
* @param string $withJavascript - use javascript if having trouble selecting an option e.g. visibility
|
||||
*/
|
||||
public function iSelectFromTheField($value, $locator)
|
||||
public function iSelectFromTheField($value, $locator, $withJavascript)
|
||||
{
|
||||
$val = str_replace('"', '\\"', $value);
|
||||
$this->getSession()->getPage()->selectFieldOption($locator, $val);
|
||||
$field = $this->getElement($locator);
|
||||
if (!$withJavascript) {
|
||||
$field->selectOption($value);
|
||||
} else {
|
||||
$xpath = $field->getXpath();
|
||||
$xpath = str_replace(['"', "\n"], ['\"', ''], $xpath);
|
||||
$value = str_replace('"', '\"', $value);
|
||||
$js = <<<JS
|
||||
return (function() {
|
||||
let select = document.evaluate("{$xpath}", document).iterateNext();
|
||||
let options = select.getElementsByTagName('option');
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
let option = options[i];
|
||||
if (option.value != "{$value}" && option.innerHTML.trim() != "{$value}") {
|
||||
continue;
|
||||
}
|
||||
select.value = option.value;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})();
|
||||
JS;
|
||||
$result = $this->getSession()->evaluateScript($js);
|
||||
Assert::assertEquals(1, $result, "Unable to select value {$value} from {$locator} with javascript");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^the rendered HTML should(| not) contain "(.+)"$/
|
||||
* @param string $not
|
||||
* @param string $htmlFragment
|
||||
*/
|
||||
public function theRenderedHtmlShouldContain($not, $htmlFragment)
|
||||
{
|
||||
$html = $this->getSession()->getPage()->getOuterHtml();
|
||||
$htmlFragment = str_replace('\"', '"', $htmlFragment);
|
||||
$contains = strpos($html, $htmlFragment) !== false;
|
||||
if ($not) {
|
||||
Assert::assertFalse($contains, "HTML fragment {$htmlFragment} was in rendered HTML when it should not have been");
|
||||
} else {
|
||||
Assert::assertTrue($contains, "HTML fragment {$htmlFragment} not found in rendered HTML");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tag values to the react TagField component which uses react-select
|
||||
*
|
||||
* @Then /^I add "([^"]+)" to the "([^"]+)" tag field$/
|
||||
* @param string $value
|
||||
* @param string $locator
|
||||
*/
|
||||
public function iAddToTheTagField($value, $locator)
|
||||
{
|
||||
$tagFieldInput = $this->getElement($locator);
|
||||
$tagFieldInput->setValue($value);
|
||||
$tagFieldInput->getParent()->getParent()->getParent()->getParent()->find('css', '.Select-menu-outer')->click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^the "([^"]+)" field should have the value "([^"]+)"$/
|
||||
* @param string $locator
|
||||
* @param string $value
|
||||
*/
|
||||
public function theFieldShouldHaveTheValue($locator, $value)
|
||||
{
|
||||
Assert::assertEquals($value, $this->getElement($locator)->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Will first attempt to find a field based on $locator
|
||||
* Will fall back to finding an element based on css selector
|
||||
*
|
||||
* @param string $locator
|
||||
* @return null|NodeElement
|
||||
*/
|
||||
private function getElement($locator): ?NodeElement
|
||||
{
|
||||
$page = $this->getSession()->getPage();
|
||||
try {
|
||||
$element = $page->findField($locator);
|
||||
} catch (ElementNotFoundException $e) {
|
||||
// noop
|
||||
}
|
||||
if (!$element) {
|
||||
$element = $page->find('css', $locator);
|
||||
}
|
||||
Assert::assertNotNull($element, "Field {$locator} was not found");
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^I drag the "([^"]+)" element to the "([^"]+)" element$/
|
||||
* @param string $locatorA
|
||||
* @param string $locatorB
|
||||
*/
|
||||
public function iDragTheElementToTheElement($locatorA, $locatorB)
|
||||
{
|
||||
$elementA = $this->getElement($locatorA);
|
||||
$elementB = $this->getElement($locatorB);
|
||||
$elementA->dragTo($elementB);
|
||||
}
|
||||
|
||||
/**
|
||||
* This doesn't seem to work quite right in practice
|
||||
* iDragTheElementToTheElement is much more reliable
|
||||
*
|
||||
* @When /^I drag the "([^"]+)" element by "(\-?[0-9]+),(\-?[0-9]+)"$/
|
||||
* @param string $locatorA
|
||||
* @param string $xOffset
|
||||
* @param string $yOffset
|
||||
*/
|
||||
public function iDragTheElementBy($locatorA, $xOffset, $yOffset)
|
||||
{
|
||||
/** @var FacebookWebDrvier $driver */
|
||||
$driver = $this->getSession()->getDriver();
|
||||
if (!($driver instanceof FacebookWebDriver)) {
|
||||
$this->logMessage('Drag and drop by offset is only supported for FacebookWebDriver: skipping');
|
||||
return;
|
||||
}
|
||||
$elementA = $this->getElement($locatorA);
|
||||
$driver->dragBy($elementA->getXpath(), (int) $xOffset, (int) $yOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Globally press the key i.e. not type into an input
|
||||
*
|
||||
* @When /^I press the "([^"]+)" key globally$/
|
||||
* @param string $keyCombo - e.g. tab / shift-tab / ctrl-c / alt-f4
|
||||
*/
|
||||
public function iPressTheKeyGlobally($keyCombo)
|
||||
{
|
||||
/** @var FacebookWebDrvier $driver */
|
||||
$driver = $this->getSession()->getDriver();
|
||||
if (!($driver instanceof FacebookWebDriver)) {
|
||||
$this->logMessage('Pressing keys globally is only supported for FacebookWebDriver: skipping');
|
||||
return;
|
||||
}
|
||||
$modifier = null;
|
||||
$pos = strpos($keyCombo, '-');
|
||||
if ($pos !== false && $pos !== 0) {
|
||||
list($modifier, $char) = explode('-', $keyCombo);
|
||||
} else {
|
||||
$char = $keyCombo;
|
||||
}
|
||||
// handle special chars e.g. "space"
|
||||
if (defined(WebDriverKeys::class . '::' . strtoupper($char))) {
|
||||
$char = constant(WebDriverKeys::class . '::' . strtoupper($char));
|
||||
}
|
||||
if ($modifier) {
|
||||
$modifier = strtoupper($modifier);
|
||||
if (defined(WebDriverKeys::class . '::' . $modifier)) {
|
||||
$modifier = constant(WebDriverKeys::class . '::' . $modifier);
|
||||
} else {
|
||||
$modifier = null;
|
||||
}
|
||||
}
|
||||
$driver->globalKeyPress($char, $modifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use upload fields
|
||||
*
|
||||
* @Then /^I attach the file "([^"]+)" to the "([^"]+)" field$/
|
||||
* @param $filename
|
||||
* @param $locator
|
||||
*/
|
||||
public function iAttachTheFileToTheField($filename, $locator)
|
||||
{
|
||||
Assert::assertNotNull($this->fixtureContext, 'FixtureContext was not found so cannot know location of fixture files');
|
||||
$path = $this->fixtureContext->getFilesPath() . '/' . $filename;
|
||||
$path = str_replace('//', '/', $path);
|
||||
Assert::assertNotEmpty($path, 'Fixture files path is empty');
|
||||
$field = $this->getElement($locator);
|
||||
$filesPath = $this->fixtureContext->getFilesPath();
|
||||
if ($filesPath) {
|
||||
$fullPath = rtrim(realpath($filesPath), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $path;
|
||||
if (is_file($fullPath)) {
|
||||
$path = $fullPath;
|
||||
}
|
||||
}
|
||||
Assert::assertFileExists($path, "{$path} does not exist");
|
||||
$field->attachFile($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to follow hyperlinks with target="_blank"
|
||||
* Behat won't switch to the new tab
|
||||
* Also allows use of css selectors
|
||||
*
|
||||
* @When /^I follow "([^"]+)" with javascript$/
|
||||
* @param string $locator
|
||||
*/
|
||||
public function iFollowWithJavascript($locator)
|
||||
{
|
||||
$page = $this->getSession()->getPage();
|
||||
$link = $page->find('named', ['link', $locator]);
|
||||
if (!$link) {
|
||||
$link = $page->find('css', $locator);
|
||||
}
|
||||
Assert::assertNotNull($link, "Link {$locator} was not found");
|
||||
$html = $link->getOuterHtml();
|
||||
preg_match('#href=([\'"])#', $html, $m);
|
||||
$q = $m[1];
|
||||
preg_match("#href={$q}(.+?){$q}#", $html, $m);
|
||||
$href = str_replace("'", "\\'", $m[1]);
|
||||
if (strpos($href, 'http') !== 0) {
|
||||
$href = rtrim($href, '/');
|
||||
$href = "/{$href}";
|
||||
}
|
||||
$this->getSession()->executeScript("document.location.href = '{$href}';");
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ use SilverStripe\Security\Group;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\MFA\Model\RegisteredMethod;
|
||||
|
||||
/**
|
||||
* LoginContext
|
||||
@ -81,6 +82,70 @@ class LoginContext implements Context
|
||||
* @param string $password
|
||||
*/
|
||||
public function stepILogInWith($email, $password)
|
||||
{
|
||||
$this->loginWith($email, $password);
|
||||
|
||||
// Check if MFA module is installed
|
||||
if (!class_exists(RegisteredMethod::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip MFA registration if MFA module installed
|
||||
$this->getMainContext()->getSession()->wait(100);
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$mfa = $this->waitForElement('#mfa-app');
|
||||
if (!$mfa) {
|
||||
return;
|
||||
}
|
||||
$clicked = false;
|
||||
$cssLocator = '.mfa-action-list__item .btn';
|
||||
$this->waitForElement($cssLocator);
|
||||
foreach ($page->findAll('css', $cssLocator) as $btn) {
|
||||
if ($btn->getText() !== 'Setup later') {
|
||||
continue;
|
||||
}
|
||||
// There's been issues clicking the button, so try waiting for a little bit
|
||||
sleep(0.3);
|
||||
$btn->click();
|
||||
$clicked = true;
|
||||
break;
|
||||
}
|
||||
assertTrue($clicked, 'MFA "Setup later" button was not found so it was not clicked');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $cssLocator
|
||||
* @return NodeElement|null
|
||||
*/
|
||||
private function waitForElement($cssLocator)
|
||||
{
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$el = null;
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$el = $page->find('css', $cssLocator);
|
||||
if ($el) {
|
||||
break;
|
||||
}
|
||||
$this->getMainContext()->getSession()->wait(100);
|
||||
}
|
||||
return $el;
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^I log in with "([^"]*)" and "([^"]*)" without skipping MFA$/
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
*/
|
||||
public function stepILogInWithWithoutSkippingMfa($email, $password)
|
||||
{
|
||||
$this->loginWith($email, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
*/
|
||||
private function loginWith($email, $password)
|
||||
{
|
||||
$c = $this->getMainContext();
|
||||
$loginUrl = $c->joinUrlParts($c->getBaseUrl(), $c->getLoginUrl());
|
||||
|
179
src/Utility/DebugTools.php
Normal file
179
src/Utility/DebugTools.php
Normal file
@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\BehatExtension\Utility;
|
||||
|
||||
use Behat\Behat\Hook\Scope\AfterScenarioScope;
|
||||
use Behat\Behat\Hook\Scope\AfterStepScope;
|
||||
use Behat\Behat\Hook\Scope\StepScope;
|
||||
use Behat\Testwork\Tester\Result\TestResult;
|
||||
use Facebook\WebDriver\Exception\WebDriverException;
|
||||
use SilverStripe\Assets\Filesystem;
|
||||
use SilverStripe\MinkFacebookWebDriver\FacebookWebDriver;
|
||||
|
||||
/**
|
||||
* Step tools to help debug failing steps
|
||||
*/
|
||||
trait DebugTools
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $takeScreenshotAfterEveryStep = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $dumpRenderedHtmlAfterEveryStep = false;
|
||||
|
||||
/**
|
||||
* Ensure utilty steps are reset for subsequent scenarios
|
||||
*
|
||||
* @AfterScenario
|
||||
* @param AfterScenarioScope $event
|
||||
*/
|
||||
public function resetUtilitiesAfterStep(AfterScenarioScope $event): void
|
||||
{
|
||||
$this->takeScreenshotAfterEveryStep = false;
|
||||
$this->dumpRenderedHtmlAfterEveryStep = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful step for working out why a behat testing isn't working when running
|
||||
* the browser headless
|
||||
* Remove this step from in a feature file once the test is working correct
|
||||
*
|
||||
* @Given /^I take a screenshot after every step$/
|
||||
*/
|
||||
public function iTakeAScreenshotAfterEveryStep()
|
||||
{
|
||||
$this->takeScreenshotAfterEveryStep = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for debugging failing behat tests
|
||||
* Remove this step from in a feature file once the test is working correct
|
||||
*
|
||||
* @Given /^I dump the rendered HTML after every step$/
|
||||
*/
|
||||
public function iDumpTheRenderedHtmlAfterEveryStep()
|
||||
{
|
||||
$this->dumpRenderedHtmlAfterEveryStep = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a screenshot when step fails, or
|
||||
* take a screenshot after every step if the use has specified
|
||||
* "I take a screenshot after every step"
|
||||
* Works only with FacebookWebDriver.
|
||||
*
|
||||
* @AfterStep
|
||||
* @param AfterStepScope $event
|
||||
*/
|
||||
public function takeScreenshotAfterFailedStep(AfterStepScope $event)
|
||||
{
|
||||
// Check failure code
|
||||
if (!$this->takeScreenshotAfterEveryStep && $event->getTestResult()->getResultCode() !== TestResult::FAILED) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->takeScreenshot($event);
|
||||
} catch (WebDriverException $e) {
|
||||
$this->logException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump HTML when step fails.
|
||||
*
|
||||
* @AfterStep
|
||||
* @param AfterStepScope $event
|
||||
*/
|
||||
public function dumpHtmlAfterStep(AfterStepScope $event): void
|
||||
{
|
||||
// Check failure code
|
||||
if ($event->getTestResult()->getResultCode() !== TestResult::FAILED && !$this->dumpRenderedHtmlAfterEveryStep) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->dumpRenderedHtml($event);
|
||||
} catch (WebDriverException $e) {
|
||||
$this->logException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump rendered HTML to disk
|
||||
* Useful for seeing the state of a page when writing and debugging feature files
|
||||
*
|
||||
* @param StepScope $event
|
||||
*/
|
||||
public function dumpRenderedHtml(StepScope $event)
|
||||
{
|
||||
$feature = $event->getFeature();
|
||||
$step = $event->getStep();
|
||||
$path = $this->prepareScreenshotPath();
|
||||
if (!$path) {
|
||||
return;
|
||||
}
|
||||
// prefix with zz_ so that it alpha sorts in the directory lower than screenshots which
|
||||
// will typically be referred to far more often. This is mainly for when you have
|
||||
// enabled `dumpRenderedHtmlAfterEveryStep`
|
||||
$path = sprintf('%s/zz_%s_%d.html', $path, basename($feature->getFile()), $step->getLine());
|
||||
$html = $this->getSession()->getPage()->getOuterHtml();
|
||||
file_put_contents($path, $html);
|
||||
$this->logMessage(sprintf('Saving HTML into %s', $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a nice screenshot
|
||||
*
|
||||
* @param StepScope $event
|
||||
*/
|
||||
public function takeScreenshot(StepScope $event)
|
||||
{
|
||||
// Validate driver
|
||||
$driver = $this->getSession()->getDriver();
|
||||
if (!($driver instanceof FacebookWebDriver)) {
|
||||
$this->logMessage('ScreenShots are only supported for FacebookWebDriver: skipping');
|
||||
return;
|
||||
}
|
||||
$feature = $event->getFeature();
|
||||
$step = $event->getStep();
|
||||
$path = $this->prepareScreenshotPath();
|
||||
if (!$path) {
|
||||
return;
|
||||
}
|
||||
$path = sprintf('%s/%s_%d.png', $path, basename($feature->getFile()), $step->getLine());
|
||||
$screenshot = $driver->getScreenshot();
|
||||
file_put_contents($path, $screenshot);
|
||||
$this->logMessage(sprintf('Saving screenshot into %s', $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the screenshots path is created
|
||||
*/
|
||||
private function prepareScreenshotPath()
|
||||
{
|
||||
// Check paths are configured
|
||||
$path = $this->getMainContext()->getScreenshotPath();
|
||||
if (!$path) {
|
||||
$this->logMessage('ScreenShots path not configured: skipping');
|
||||
return;
|
||||
}
|
||||
Filesystem::makeFolder($path);
|
||||
$path = realpath($path);
|
||||
if (!file_exists($path)) {
|
||||
$this->logMessage(sprintf('"%s" is not valid directory and failed to create it', $path));
|
||||
return;
|
||||
}
|
||||
if (file_exists($path) && !is_dir($path)) {
|
||||
$this->logMessage(sprintf('"%s" is not valid directory', $path));
|
||||
return;
|
||||
}
|
||||
if (file_exists($path) && !is_writable($path)) {
|
||||
$this->logMessage(sprintf('"%s" directory is not writable', $path));
|
||||
return;
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user