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;
|
namespace SilverStripe\BehatExtension\Context;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use InvalidArgumentException;
|
||||||
use Behat\Behat\Context\Context;
|
use Behat\Behat\Context\Context;
|
||||||
|
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
|
||||||
use Behat\Behat\Definition\Call;
|
use Behat\Behat\Definition\Call;
|
||||||
use Behat\Behat\Hook\Scope\AfterScenarioScope;
|
use Behat\Behat\Hook\Scope\AfterScenarioScope;
|
||||||
use Behat\Behat\Hook\Scope\AfterStepScope;
|
use Behat\Behat\Hook\Scope\AfterStepScope;
|
||||||
|
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
|
||||||
use Behat\Behat\Hook\Scope\BeforeStepScope;
|
use Behat\Behat\Hook\Scope\BeforeStepScope;
|
||||||
use Behat\Behat\Hook\Scope\StepScope;
|
|
||||||
use Behat\Mink\Element\NodeElement;
|
use Behat\Mink\Element\NodeElement;
|
||||||
|
use Behat\Mink\Exception\ElementNotFoundException;
|
||||||
use Behat\Mink\Session;
|
use Behat\Mink\Session;
|
||||||
use Behat\Testwork\Tester\Result\TestResult;
|
use Behat\Testwork\Tester\Result\TestResult;
|
||||||
use Exception;
|
|
||||||
use Facebook\WebDriver\Exception\WebDriverException;
|
use Facebook\WebDriver\Exception\WebDriverException;
|
||||||
use Facebook\WebDriver\WebDriver;
|
use Facebook\WebDriver\WebDriver;
|
||||||
use Facebook\WebDriver\WebDriverAlert;
|
use Facebook\WebDriver\WebDriverAlert;
|
||||||
use Facebook\WebDriver\WebDriverExpectedCondition;
|
use Facebook\WebDriver\WebDriverExpectedCondition;
|
||||||
use InvalidArgumentException;
|
use Facebook\WebDriver\WebDriverKeys;
|
||||||
use PHPUnit\Framework\Assert;
|
use PHPUnit\Framework\Assert;
|
||||||
use SilverStripe\Assets\File;
|
use SilverStripe\Assets\File;
|
||||||
use SilverStripe\Assets\Filesystem;
|
use SilverStripe\Assets\Filesystem;
|
||||||
use SilverStripe\BehatExtension\Utility\StepHelper;
|
use SilverStripe\BehatExtension\Utility\StepHelper;
|
||||||
|
use SilverStripe\BehatExtension\Utility\DebugTools;
|
||||||
use SilverStripe\MinkFacebookWebDriver\FacebookWebDriver;
|
use SilverStripe\MinkFacebookWebDriver\FacebookWebDriver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,6 +39,7 @@ class BasicContext implements Context
|
|||||||
{
|
{
|
||||||
use MainContextAwareTrait;
|
use MainContextAwareTrait;
|
||||||
use StepHelper;
|
use StepHelper;
|
||||||
|
use DebugTools;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Date format in date() syntax
|
* Date format in date() syntax
|
||||||
@ -55,6 +60,50 @@ class BasicContext implements Context
|
|||||||
*/
|
*/
|
||||||
protected $datetimeFormat = 'Y-m-d H:i:s';
|
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
|
* Get Mink session from MinkContext
|
||||||
*
|
*
|
||||||
@ -129,7 +178,7 @@ JS;
|
|||||||
$jserrors = $page->find('xpath', '//body[@data-jserrors]');
|
$jserrors = $page->find('xpath', '//body[@data-jserrors]');
|
||||||
if (null !== $jserrors) {
|
if (null !== $jserrors) {
|
||||||
$this->takeScreenshot($event);
|
$this->takeScreenshot($event);
|
||||||
file_put_contents('php://stderr', $jserrors->getAttribute('data-jserrors') . PHP_EOL);
|
$this->logMessage($jserrors->getAttribute('data-jserrors'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$javascript = <<<JS
|
$javascript = <<<JS
|
||||||
@ -250,26 +299,6 @@ JS;
|
|||||||
$this->getSession()->wait(100);
|
$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
|
* Close modal dialog if test scenario fails on CMS page
|
||||||
*
|
*
|
||||||
@ -313,54 +342,6 @@ JS;
|
|||||||
Filesystem::removeFolder(ASSETS_PATH, true);
|
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/
|
* @Given /^the page can't be found/
|
||||||
*/
|
*/
|
||||||
@ -382,7 +363,7 @@ JS;
|
|||||||
*/
|
*/
|
||||||
public function stepIWaitFor($secs)
|
public function stepIWaitFor($secs)
|
||||||
{
|
{
|
||||||
$this->getSession()->wait((float)$secs*1000);
|
$this->getSession()->wait((float)$secs * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -631,7 +612,7 @@ JS;
|
|||||||
protected function getWebDriverSession()
|
protected function getWebDriverSession()
|
||||||
{
|
{
|
||||||
$driver = $this->getSession()->getDriver();
|
$driver = $this->getSession()->getDriver();
|
||||||
if (! $driver instanceof FacebookWebDriver) {
|
if (!$driver instanceof FacebookWebDriver) {
|
||||||
throw new InvalidArgumentException("Only supported for FacebookWebDriver");
|
throw new InvalidArgumentException("Only supported for FacebookWebDriver");
|
||||||
}
|
}
|
||||||
return $driver->getWebDriver();
|
return $driver->getWebDriver();
|
||||||
@ -642,6 +623,8 @@ JS;
|
|||||||
* @param string $field
|
* @param string $field
|
||||||
* @param string $path
|
* @param string $path
|
||||||
* @return Call\Given
|
* @return Call\Given
|
||||||
|
*
|
||||||
|
* @deprecated 4.5..5.0 - use iAttachTheFileToTheField() instead
|
||||||
*/
|
*/
|
||||||
public function iAttachTheFileTo($field, $path)
|
public function iAttachTheFileTo($field, $path)
|
||||||
{
|
{
|
||||||
@ -925,7 +908,6 @@ JS;
|
|||||||
$regionObj->fillField($field, $value);
|
$regionObj->fillField($field, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts text in a specific region (an element identified by a CSS selector, a "data-title" attribute,
|
* 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).
|
* or a named region mapped to a CSS selector via Behat configuration).
|
||||||
@ -948,7 +930,7 @@ JS;
|
|||||||
|
|
||||||
$actual = $regionObj->getText();
|
$actual = $regionObj->getText();
|
||||||
$actual = preg_replace('/\s+/u', ' ', $actual);
|
$actual = preg_replace('/\s+/u', ' ', $actual);
|
||||||
$regex = '/'.preg_quote($text, '/').'/ui';
|
$regex = '/' . preg_quote($text, '/') . '/ui';
|
||||||
|
|
||||||
if (trim($negate)) {
|
if (trim($negate)) {
|
||||||
if (preg_match($regex, $actual)) {
|
if (preg_match($regex, $actual)) {
|
||||||
@ -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
|
* We have to catch exceptions and log somehow else otherwise behat falls over
|
||||||
*
|
*
|
||||||
@ -1288,7 +1280,7 @@ JS;
|
|||||||
*/
|
*/
|
||||||
protected function logException(Exception $exception)
|
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 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
|
* 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
|
* @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
|
$js = <<<JS
|
||||||
return document.querySelector("$sel");
|
return document.querySelector("$sel");
|
||||||
JS;
|
JS;
|
||||||
$element = $this->getSession()->evaluateScript($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.
|
* Selects the option in select field with specified id|name|label|value
|
||||||
* Note: this is duplicate code from SilverStripeContext selectOption
|
* Also accepts CSS selectors
|
||||||
* In practice, the primary context file using in modules have inherited from BasicContext
|
|
||||||
* and not SilverStripeContext so the selectOption method is not available.
|
|
||||||
*
|
*
|
||||||
* @When /^I select "([^"]+)" from the "([^"]+)" field$/
|
* @When /^I select "([^"]+)" from the "([^"]+)" field(| with javascript)$/
|
||||||
* @param string $value
|
* @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);
|
$field = $this->getElement($locator);
|
||||||
$this->getSession()->getPage()->selectFieldOption($locator, $val);
|
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\Member;
|
||||||
use SilverStripe\Security\Permission;
|
use SilverStripe\Security\Permission;
|
||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
|
use SilverStripe\MFA\Model\RegisteredMethod;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LoginContext
|
* LoginContext
|
||||||
@ -81,6 +82,70 @@ class LoginContext implements Context
|
|||||||
* @param string $password
|
* @param string $password
|
||||||
*/
|
*/
|
||||||
public function stepILogInWith($email, $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();
|
$c = $this->getMainContext();
|
||||||
$loginUrl = $c->joinUrlParts($c->getBaseUrl(), $c->getLoginUrl());
|
$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