API Upgrade to behat 3

This commit is contained in:
Ingo Schommer 2014-08-02 18:30:27 +12:00 committed by Sam Minnée
parent 78c65719da
commit 9230ce2405
37 changed files with 1866 additions and 1368 deletions

View File

@ -10,8 +10,9 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{*.yml,package.json}]
[*.{yml,js,json,css,scss,eslintrc}]
indent_size = 2
indent_style = space
# The indent size used in the package.json file cannot be changed:
# https://github.com/npm/npm/pull/3180#issuecomment-16336516

View File

@ -16,17 +16,16 @@ matrix:
env: PHPUNIT_TEST=1
- php: 7.0
env: PHPUNIT_TEST=1
- php: 7.1
- php: 7.1.2
env: PHPUNIT_TEST=1
before_script:
- composer validate
- composer install --dev --prefer-dist
- composer require silverstripe/config:1.0.x-dev silverstripe/framework:4.0.x-dev --prefer-dist
- "if [ \"$PHPCS_TEST\" = \"1\" ]; then pyrus install pear/PHP_CodeSniffer; fi"
- if [[ $PHPCS_TEST ]]; then pyrus install pear/PHP_CodeSniffer; fi
- phpenv rehash
script:
- "if [ \"$PHPUNIT_TEST\" = \"1\" ]; then vendor/bin/phpunit tests; fi"
- "if [ \"$PHPCS_TEST\" = \"1\" ]; then phpcs --standard=PSR2 -n src/ tests/; fi"
- if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit tests/php; fi
- if [[ $PHPCS_TEST ]]; then composer run-script lint; fi

View File

@ -40,7 +40,7 @@ Note: The extension has only been tested with the `selenium2` Mink driver.
Simply [install SilverStripe through Composer](http://doc.silverstripe.org/framework/en/installation/composer).
Skip this step if adding the module to an existing project.
composer create-project silverstripe/installer my-test-project 3.x-dev
composer create-project silverstripe/installer my-test-project 4.x-dev
Switch to the newly created webroot, and add the SilverStripe Behat extension.
@ -94,10 +94,6 @@ Now you can run the tests (for example for the `framework` module):
vendor/bin/behat @framework
In order to run specific tests only, use their feature file name:
vendor/bin/behat @framework/login.feature
Or even run a single scenario by it's name (supports regular expressions):
vendor/bin/behat --name 'My scenario title' @framework
@ -118,19 +114,13 @@ The SilverStripe installer already comes with a YML configuration
which is ready to run tests on a locally hosted Selenium server,
located in the project root as `behat.yml`.
You'll need to customize at least the `base_url` setting to match the URL where
the tested SilverStripe instance is hosted locally. This
You should ensure that you have configured SS_BASE_URL in your `.env`.
Generic Mink configuration settings are placed in `SilverStripe\BehatExtension\MinkExtension`,
which is a subclass of `Behat\MinkExtension\Extension`.
Overview of settings (all in the `extensions.SilverStripe\BehatExtension\Extension` path):
* `framework_path`: Path to the SilverStripe Framework folder. It supports both absolute and relative (to `behat.yml` file) paths.
* `extensions.Behat\MinkExtension\Extension.base_url`: You will probably need to change the base URL that is used during the test process.
It is used every time you use relative URLs in your feature descriptions.
It will also be used by [file to URL mapping](http://doc.silverstripe.org/framework/en/topics/commandline#configuration) in `SilverStripeExtension`.
* `extensions.Behat\MinkExtension\Extension.files_path`: Change to support file uploads in your tests. Currently only absolute paths are supported.
* `ajax_steps`: Because SilverStripe uses AJAX requests quite extensively, we had to invent a way
to deal with them more efficiently and less verbose than just
Optional `ajax_steps` is used to match steps defined there so they can be "caught" by
@ -145,18 +135,28 @@ number that failed.
Example: behat.yml
default:
context:
class: SilverStripe\MyModule\Test\Behaviour\FeatureContext
suites:
framework:
paths:
- %paths.modules.framework%/tests/behat/features
contexts:
- SilverStripe\Framework\Tests\Behaviour\FeatureContext
- SilverStripe\Framework\Tests\Behaviour\CmsFormsContext
- SilverStripe\Framework\Tests\Behaviour\CmsUiContext
- SilverStripe\BehatExtension\Context\BasicContext
- SilverStripe\BehatExtension\Context\EmailContext
- SilverStripe\BehatExtension\Context\LoginContext
-
SilverStripe\BehatExtension\Context\FixtureContext:
- %paths.modules.framework%/tests/behat/features/files/
extensions:
SilverStripe\BehatExtension\Extension:
screenshot_path: %behat.paths.base%/artifacts/screenshots
SilverStripe\BehatExtension\MinkExtension:
# Adjust this to your local environment
base_url: http://localhost/
default_session: selenium2
javascript_session: selenium2
selenium2:
browser: firefox
SilverStripe\BehatExtension\Extension:
screenshot_path: %paths.base%/artifacts/screenshots
## Module Initialization
@ -172,18 +172,14 @@ Since step definitions are quite domain specific, its likely that you'll need yo
The SilverStripe Behat extension provides an initializer script which generates a template
in the recommended folder structure:
vendor/bin/behat --init @mymodule
vendor/bin/behat --init @mymodule --namespace="MyVendor\MyModule"
You'll now have a class located in `mymodule/tests/behat/features/bootstrap/Context/FeatureContext.php`,
as well as a folder for your features with `mymodule/tests/behat/features`.
The class is namespaced, and defaults to the module name. You can customize this:
Note: namespace is mandatory
vendor/bin/behat --namespace='MyVendor\MyModule' --init @mymodule
In this case, you'll need to pass in the namespace when running the features as well
(at least until SilverStripe modules allow declaring a namespace).
vendor/bin/behat --namespace='MyVendor\MyModule' @mymodule
You'll now have a class located in `mymodule/tests/behat/src/FeatureContext.php`,
which will have a psr-4 class mapping added to composer.json by default.
Also a folder for your features with `mymodule/tests/behat/features` will be created.
A `mymodule/behat.yml` is built, with a default suite named after the module.
## Available Step Definitions
@ -265,9 +261,10 @@ use the inline definition syntax. The following example shows some syntax variat
### Directory Structure
As a convention, SilverStripe Behat tests live in a `tests/behat` subfolder
of your module. You can create it with the following command:
of your module. You can create it with the following commands:
mkdir -p mymodule/tests/behat/features/bootstrap/MyModule/Test/Behaviour
mkdir -p mymodule/tests/behat/features/
mkdir -p mymodule/tests/behat/src/
### FeatureContext
@ -276,24 +273,15 @@ here as well. The only major difference is the base class from which
to extend your own `FeatureContext`: It should be `SilverStripeContext`
rather than `BehatContext`.
Example: mymodule/tests/behat/features/bootstrap/MyModule/Test/Behaviour/FeatureContext.php
Example: mymodule/tests/behat/src/FeatureContext.php
<?php
namespace MyModule\Test\Behaviour;
use SilverStripe\BehatExtension\Context\SilverStripeContext,
SilverStripe\BehatExtension\Context\BasicContext,
SilverStripe\BehatExtension\Context\LoginContext;
use SilverStripe\BehatExtension\Context\SilverStripeContext;
class FeatureContext extends SilverStripeContext
{
public function __construct(array $parameters)
{
$this->useContext('BasicContext', new BasicContext($parameters));
$this->useContext('LoginContext', new LoginContext($parameters));
parent::__construct($parameters);
}
}
### Screen Size

View File

@ -2,7 +2,12 @@
"name": "silverstripe/behat-extension",
"type": "behat-extension",
"description": "SilverStripe framework extension for Behat",
"keywords": ["framework", "web", "bdd", "silverstripe"],
"keywords": [
"framework",
"web",
"bdd",
"silverstripe"
],
"homepage": "http://silverstripe.org",
"license": "MIT",
"authors": [
@ -15,27 +20,26 @@
"email": "ingo@silverstripe.com"
}
],
"require": {
"php": ">=5.3.3",
"phpunit/phpunit": "^4.8 || ^5.7",
"behat/behat": "~2.5.0",
"behat/mink": "~1.6.0",
"behat/mink-extension": "~1.3.0",
"behat/mink-selenium2-driver": "~1.2.0",
"symfony/dom-crawler": "~2.0",
"php": ">=5.6",
"phpunit/phpunit": "^5.7",
"behat/behat": "^3.2",
"behat/mink": "^1.7",
"behat/mink-extension": "^2.1",
"behat/mink-selenium2-driver": "^1.3",
"symfony/dom-crawler": "^3",
"silverstripe/testsession": "2.0.0-alpha6",
"silverstripe/framework": "^4.0.0@dev"
"silverstripe/framework": "^4@dev",
"symfony/finder": "^3.2"
},
"autoload": {
"psr-0": {
"SilverStripe\\BehatExtension": "src/"
"psr-4": {
"SilverStripe\\BehatExtension\\": "src/"
}
},
"autoload-dev": {
"psr-0": {
"SilverStripe\\BehatExtension\\Tests": "tests/"
"SilverStripe\\BehatExtension\\Tests\\": "tests/php/"
},
"classmap": [
"framework",
@ -44,9 +48,12 @@
},
"extra": {
"branch-alias": {
"dev-master": "2.2.x-dev"
"dev-master": "3.x-dev"
}
},
"scripts": {
"lint": "phpcs --standard=PSR2 -n src/ tests/php/"
},
"prefer-stable": true,
"minimum-stability": "dev"
}

29
config/silverstripe.yml Normal file
View File

@ -0,0 +1,29 @@
parameters:
silverstripe_extension.context.initializer.class: SilverStripe\BehatExtension\Context\Initializer\SilverStripeAwareInitializer
# Moved to PHP. See Extension::load()
# console.processor.locator.class: SilverStripe\BehatExtension\Controllers\LocatorProcessor
# Custom init processory temporarily removed
# console.processor.init.class: SilverStripe\BehatExtension\Controllers\InitProcessor
silverstripe_extension.ajax_steps: ~
silverstripe_extension.ajax_timeout: ~
silverstripe_extension.admin_url: ~
silverstripe_extension.login_url: ~
silverstripe_extension.screenshot_path: ~
silverstripe_extension.module:
silverstripe_extension.region_map: ~
silverstripe_extension.context.namespace_suffix: Tests\Behaviour
silverstripe_extension.context.features_path: tests/behat/features/
silverstripe_extension.context.class_path: tests/behat/src/
services:
silverstripe_extension.context.initializer:
class: %silverstripe_extension.context.initializer.class%
calls:
- [setAjaxSteps, [%silverstripe_extension.ajax_steps%]]
- [setAjaxTimeout, [%silverstripe_extension.ajax_timeout%]]
- [setAdminUrl, [%silverstripe_extension.admin_url%]]
- [setLoginUrl, [%silverstripe_extension.login_url%]]
- [setScreenshotPath, [%silverstripe_extension.screenshot_path%]]
- [setRegionMap, [%silverstripe_extension.region_map%]]
tags:
- { name: context.initializer }

View File

@ -2,7 +2,7 @@
namespace SilverStripe\BehatExtension\Compiler;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Core\Manifest\ModuleLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
@ -26,11 +26,21 @@ class CoreInitializationPass implements CompilerPassInterface
require_once('Core/Core.php');
// Include bootstrap file
$bootstrapFile = $container->getParameter('behat.silverstripe_extension.bootstrap_file');
$bootstrapFile = $container->getParameter('silverstripe_extension.bootstrap_file');
if ($bootstrapFile) {
require_once $bootstrapFile;
}
// Register all paths
foreach (ModuleLoader::instance()->getManifest()->getModules() as $module) {
$container->setParameter('paths.modules.'.$module->getShortName(), $module->getPath());
$composerName = $module->getComposerName();
if ($composerName) {
list($vendor,$name) = explode('/', $composerName);
$container->setParameter('paths.modules.'.$vendor.'.'.$name, $module->getPath());
}
}
unset($_GET['flush']);
// Remove the error handler so that PHPUnit can add its own

View File

@ -0,0 +1,40 @@
<?php
namespace SilverStripe\BehatExtension\Compiler;
use InvalidArgumentException;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Behat\SilverStripe container compilation pass.
* Passes Base URL available in MinkExtension config.
* Used for the {@link \SilverStripe\BehatExtension\MinkExtension} subclass.
*
* @author Michał Ochman <ochman.d.michal@gmail.com>
*/
class MinkExtensionBaseUrlPass implements CompilerPassInterface
{
/**
* Passes MinkExtension's base_url parameter
*
* @param ContainerBuilder $container
*/
public function process(ContainerBuilder $container)
{
// Set url from environment
$baseURL = getenv('SS_BASE_URL');
if (!$baseURL) {
throw new InvalidArgumentException(
'"base_url" not configured. Please specify it in your .env config with SS_BASE_URL'
);
}
$container->setParameter('mink.base_url', $baseURL);
// The Behat\MinkExtension\Extension class copies configuration into an internal hash,
// we need to follow this pattern to propagate our changes.
$parameters = $container->getParameter('mink.parameters');
$parameters['base_url'] = $container->getParameter('mink.base_url');
$container->setParameter('mink.parameters', $parameters);
}
}

View File

@ -2,19 +2,23 @@
namespace SilverStripe\BehatExtension\Context;
use Behat\Behat\Context\BehatContext;
use Behat\Behat\Context\Step;
use Behat\Behat\Event\StepEvent;
use Behat\Behat\Event\ScenarioEvent;
use Behat\Behat\Context\Context;
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\Gherkin\Node\ScenarioNode;
use Behat\Mink\Driver\Selenium2Driver;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Session;
use Behat\MinkExtension\Context\RawMinkContext;
use Behat\Testwork\Tester\Result\TestResult;
use Exception;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Filesystem;
// PHPUnit
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
use WebDriver\Exception as WebDriverException;
use WebDriver\Session as WebDriverSession;
/**
* BasicContext
@ -24,13 +28,21 @@ require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions
* Handles redirections.
* Handles AJAX enabled links, buttons and forms - jQuery is assumed.
*/
class BasicContext extends BehatContext
class BasicContext implements Context
{
protected $context;
use MainContextAwareTrait;
/**
* Work-around for https://github.com/Behat/Behat/issues/653
*
* @var ScenarioNode
*/
protected $currentScenario = null;
/**
* Date format in date() syntax
* @var String
*
* @var string
*/
protected $dateFormat = 'Y-m-d';
@ -46,18 +58,6 @@ class BasicContext extends BehatContext
*/
protected $datetimeFormat = 'Y-m-d H:i:s';
/**
* Initializes context.
* Every scenario gets it's own context object.
*
* @param array $parameters context parameters (set them up through behat.yml)
*/
public function __construct(array $parameters)
{
// Initialize your context here
$this->context = $parameters;
}
/**
* Get Mink session from MinkContext
*
@ -72,13 +72,42 @@ class BasicContext extends BehatContext
}
/**
* @AfterStep ~@modal
* Work-around for https://github.com/Behat/Behat/issues/653
*
* @BeforeScenario
* @param BeforeScenarioScope $event
*/
public function handleScenarioBegin(BeforeScenarioScope $event)
{
$this->currentScenario = $event->getScenario();
}
/**
* Work-around for https://github.com/Behat/Behat/issues/653
*
* @AfterScenario
* @param AfterScenarioScope $event
*/
public function handleScenarioEnd(AfterScenarioScope $event)
{
$this->currentScenario = null;
}
/**
* @AfterStep
*
* Excluding scenarios with @modal tag is required,
* because modal dialogs stop any JS interaction
*
* @param AfterStepScope $event
*/
public function appendErrorHandlerBeforeStep(StepEvent $event)
public function appendErrorHandlerBeforeStep(AfterStepScope $event)
{
// Manually exclude @modal
if ($this->stepHasTag($event, 'modal')) {
return;
}
try {
$javascript = <<<JS
window.onerror = function(message, file, line, column, error) {
@ -88,29 +117,37 @@ window.onerror = function(message, file, line, column, error) {
msg += "\\nSTACKTRACE:\\n" + error.stack;
}
body.setAttribute('data-jserrors', '[captured JavaScript error] ' + msg);
}
};
if ('undefined' !== typeof window.jQuery) {
window.jQuery('body').ajaxError(function(event, jqxhr, settings, exception) {
if ('abort' === exception) return;
if ('abort' === exception) {
return;
}
window.onerror(event.type + ': ' + settings.type + ' ' + settings.url + ' ' + exception + ' ' + jqxhr.responseText);
});
}
JS;
$this->getSession()->executeScript($javascript);
} catch (\WebDriver\Exception $e) {
} catch (WebDriverException $e) {
$this->logException($e);
}
}
/**
* @AfterStep ~@modal
* @AfterStep
*
* Excluding scenarios with @modal tag is required,
* because modal dialogs stop any JS interaction
*
* @param AfterStepScope $event
*/
public function readErrorHandlerAfterStep(StepEvent $event)
public function readErrorHandlerAfterStep(AfterStepScope $event)
{
// Manually exclude @modal
if ($this->stepHasTag($event, 'modal')) {
return;
}
try {
$page = $this->getSession()->getPage();
@ -129,7 +166,7 @@ if ('undefined' !== typeof window.jQuery) {
JS;
$this->getSession()->executeScript($javascript);
} catch (\WebDriver\Exception $e) {
} catch (WebDriverException $e) {
$this->logException($e);
}
}
@ -140,9 +177,14 @@ JS;
* Event handlers are removed after one run.
*
* @BeforeStep
* @param BeforeStepScope $event
*/
public function handleAjaxBeforeStep(StepEvent $event)
public function handleAjaxBeforeStep(BeforeStepScope $event)
{
// Manually exclude @modal
if ($this->stepHasTag($event, 'modal')) {
return;
}
try {
$ajaxEnabledSteps = $this->getMainContext()->getAjaxSteps();
$ajaxEnabledSteps = implode('|', array_filter($ajaxEnabledSteps));
@ -176,7 +218,7 @@ if ('undefined' !== typeof window.jQuery && 'undefined' !== typeof window.jQuery
JS;
$this->getSession()->wait(500); // give browser a chance to process and render response
$this->getSession()->executeScript($javascript);
} catch (\WebDriver\Exception $e) {
} catch (WebDriverException $e) {
$this->logException($e);
}
}
@ -187,10 +229,15 @@ JS;
*
* Don't unregister handler if we're dealing with modal windows
*
* @AfterStep ~@modal
* @AfterStep
* @param AfterStepScope $event
*/
public function handleAjaxAfterStep(StepEvent $event)
public function handleAjaxAfterStep(AfterStepScope $event)
{
// Manually exclude @modal
if ($this->stepHasTag($event, 'modal')) {
return;
}
try {
$ajaxEnabledSteps = $this->getMainContext()->getAjaxSteps();
$ajaxEnabledSteps = implode('|', array_filter($ajaxEnabledSteps));
@ -209,7 +256,7 @@ window.jQuery(document).off('ajaxSuccess.ss.test.behaviour');
}
JS;
$this->getSession()->executeScript($javascript);
} catch (\WebDriver\Exception $e) {
} catch (WebDriverException $e) {
$this->logException($e);
}
}
@ -233,40 +280,44 @@ JS;
* Works only with Selenium2Driver.
*
* @AfterStep
* @param AfterStepScope $event
*/
public function takeScreenshotAfterFailedStep(StepEvent $event)
public function takeScreenshotAfterFailedStep(AfterStepScope $event)
{
if (4 === $event->getResult()) {
// Check failure code
if ($event->getTestResult()->getResultCode() !== TestResult::FAILED) {
return;
}
try {
$this->takeScreenshot($event);
} catch (\WebDriver\Exception $e) {
} catch (WebDriverException $e) {
$this->logException($e);
}
}
}
/**
* Close modal dialog if test scenario fails on CMS page
*
* @AfterScenario
* @param AfterScenarioScope $event
*/
public function closeModalDialog(ScenarioEvent $event)
public function closeModalDialog(AfterScenarioScope $event)
{
try {
// Only for failed tests on CMS page
if (4 === $event->getResult()) {
if ($event->getTestResult()->getResultCode() === TestResult::FAILED) {
$cmsElement = $this->getSession()->getPage()->find('css', '.cms');
if ($cmsElement) {
try {
// Navigate away triggered by reloading the page
$this->getSession()->reload();
$this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
} catch (\WebDriver\Exception $e) {
$this->getWebDriverSession()->accept_alert();
} catch (WebDriverException $e) {
// no-op, alert might not be present
}
}
}
} catch (\WebDriver\Exception $e) {
} catch (WebDriverException $e) {
$this->logException($e);
}
}
@ -275,8 +326,9 @@ JS;
* Delete any created files and folders from assets directory
*
* @AfterScenario @assets
* @param AfterScenarioScope $event
*/
public function cleanAssetsAfterScenario(ScenarioEvent $event)
public function cleanAssetsAfterScenario(AfterScenarioScope $event)
{
foreach (File::get() as $file) {
$file->delete();
@ -284,23 +336,30 @@ JS;
Filesystem::removeFolder(ASSETS_PATH, true);
}
public function takeScreenshot(StepEvent $event)
/**
* Take a nice screenshot
*
* @param StepScope $event
*/
public function takeScreenshot(StepScope $event)
{
// Validate driver
$driver = $this->getSession()->getDriver();
// quit silently when unsupported
if (!($driver instanceof Selenium2Driver)) {
file_put_contents('php://stdout', 'ScreenShots are only supported for Selenium2Driver: skipping');
return;
}
$parent = $event->getLogicalParent();
$feature = $parent->getFeature();
$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;
} // quit silently when path is not set
}
Filesystem::makeFolder($path);
$path = realpath($path);
@ -325,22 +384,6 @@ JS;
file_put_contents('php://stderr', sprintf('Saving screenshot into %s' . PHP_EOL, $path));
}
/**
* @Then /^I should be redirected to "([^"]+)"/
*/
public function stepIShouldBeRedirectedTo($url)
{
if ($this->getMainContext()->canIntercept()) {
$client = $this->getSession()->getDriver()->getClient();
$client->followRedirects(true);
$client->followRedirect();
$url = $this->getMainContext()->joinUrlParts($this->context['base_url'], $url);
assertTrue($this->getMainContext()->isCurrentUrlSimilarTo($url), sprintf('Current URL is not %s', $url));
}
}
/**
* @Given /^the page can't be found/
*/
@ -357,6 +400,8 @@ JS;
/**
* @Given /^I wait (?:for )?([\d\.]+) second(?:s?)$/
*
* @param float $secs
*/
public function stepIWaitFor($secs)
{
@ -367,7 +412,7 @@ JS;
* Find visible button with the given text.
* Supports data-text-alternate property.
*
* @param string $text
* @param string $title
* @return NodeElement|null
*/
protected function findNamedButton($title)
@ -382,9 +427,10 @@ JS;
];
foreach ($searches as list($type, $arg)) {
$buttons = $page->findAll($type, $arg);
foreach ($buttons as $el) {
if ($el->isVisible()) {
return $el;
/** @var NodeElement $button */
foreach ($buttons as $button) {
if ($button->isVisible()) {
return $button;
}
}
}
@ -396,6 +442,8 @@ JS;
* Example: I should not see a "Delete" button
*
* @Given /^I should( not? |\s*)see (?:a|an|the) "([^"]*)" button$/
* @param string $negative
* @param string $text
*/
public function iShouldSeeAButton($negative, $text)
{
@ -409,6 +457,7 @@ JS;
/**
* @Given /^I press the "([^"]*)" button$/
* @param string $text
*/
public function stepIPressTheButton($text)
{
@ -423,6 +472,7 @@ JS;
* Example2: I follow the "Remove current combo" link, confirming the dialog
*
* @Given /^I (?:press|follow) the "([^"]*)" (?:button|link), confirming the dialog$/
* @param string $button
*/
public function stepIPressTheButtonConfirmingTheDialog($button)
{
@ -435,6 +485,7 @@ JS;
* Example: I follow the "Remove current combo" link, dismissing the dialog
*
* @Given /^I (?:press|follow) the "([^"]*)" (?:button|link), dismissing the dialog$/
* @param string $button
*/
public function stepIPressTheButtonDismissingTheDialog($button)
{
@ -444,6 +495,9 @@ JS;
/**
* @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element$/
* @param string $clickType
* @param string $text
* @param string $selector
*/
public function iClickInTheElement($clickType, $text, $selector)
{
@ -465,17 +519,24 @@ JS;
* Example: I click "Delete" in the ".actions" element, confirming the dialog
*
* @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, confirming the dialog$/
* @param string $clickType
* @param string $text
* @param string $selector
*/
public function iClickInTheElementConfirmingTheDialog($clickType, $text, $selector)
{
$this->iClickInTheElement($clickType, $text, $selector);
$this->iConfirmTheDialog();
}
/**
* Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
* Example: I click "Delete" in the ".actions" element, dismissing the dialog
*
* @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, dismissing the dialog$/
* @param string $clickType
* @param string $text
* @param string $selector
*/
public function iClickInTheElementDismissingTheDialog($clickType, $text, $selector)
{
@ -485,6 +546,7 @@ JS;
/**
* @Given /^I see the text "([^"]+)" in the alert$/
* @param string $expected
*/
public function iSeeTheDialogText($expected)
{
@ -497,13 +559,11 @@ JS;
/**
* @Given /^I type "([^"]*)" into the dialog$/
* @param string $data
*/
public function iTypeIntoTheDialog($data)
{
$data = array(
'text' => $data,
);
$this->getSession()->getDriver()->getWebDriverSession()->postAlert_text($data);
$this->getWebDriverSession()->postAlert_text([ 'text' => $data ]);
}
/**
@ -511,7 +571,7 @@ JS;
*/
public function iConfirmTheDialog()
{
$this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
$this->getWebDriverSession()->accept_alert();
$this->handleAjaxTimeout();
}
@ -520,18 +580,36 @@ JS;
*/
public function iDismissTheDialog()
{
$this->getSession()->getDriver()->getWebDriverSession()->dismiss_alert();
$this->getWebDriverSession()->dismiss_alert();
$this->handleAjaxTimeout();
}
/**
* Get Selenium webdriver session.
* Note: Will fail if current driver isn't Selenium2Driver
*
* @return WebDriverSession
*/
protected function getWebDriverSession()
{
$driver = $this->getSession()->getDriver();
if (! $driver instanceof Selenium2Driver) {
throw new \InvalidArgumentException("Not supported for non-selenium2 drivers");
}
return $driver->getWebDriverSession();
}
/**
* @Given /^(?:|I )attach the file "(?P<path>[^"]*)" to "(?P<field>(?:[^"]|\\")*)" with HTML5$/
* @param string $field
* @param string $path
* @return Call\Given
*/
public function iAttachTheFileTo($field, $path)
{
// Remove wrapped button styling to make input field accessible to Selenium
$js = <<<JS
var input = jQuery('[name="$field"]');
let input = jQuery('[name="$field"]');
if(input.closest('.ss-uploadfield-item-info').length) {
while(!input.parent().is('.ss-uploadfield-item-info')) input = input.unwrap();
}
@ -540,19 +618,22 @@ JS;
$this->getSession()->executeScript($js);
$this->getSession()->wait(1000);
return new Step\Given(sprintf('I attach the file "%s" to "%s"', $path, $field));
return $this->getMainContext()->attachFileToField($field, $path);
}
/**
* Select an individual input from within a group, matched by the top-most label.
*
* @Given /^I select "([^"]*)" from "([^"]*)" input group$/
* @param string $value
* @param string $labelText
*/
public function iSelectFromInputGroup($value, $labelText)
{
$page = $this->getSession()->getPage();
$parent = null;
/** @var NodeElement $label */
foreach ($page->findAll('css', 'label') as $label) {
if ($label->getText() == $labelText) {
$parent = $label->getParent();
@ -563,6 +644,7 @@ JS;
throw new \InvalidArgumentException(sprintf('Input group with label "%s" cannot be found', $labelText));
}
/** @var NodeElement $option */
foreach ($parent->findAll('css', 'label') as $option) {
if ($option->getText() == $value) {
$input = null;
@ -608,6 +690,9 @@ JS;
* Customize through {@link setTimeFormat()}.
*
* @Transform /^(?:(the|a)) time of (?<val>.*)$/
* @param string $prefix
* @param string $val
* @return false|string
*/
public function castRelativeToAbsoluteTime($prefix, $val)
{
@ -627,6 +712,9 @@ JS;
* the 12th of October 2013. Customize through {@link setDatetimeFormat()}.
*
* @Transform /^(?:(the|a)) datetime of (?<val>.*)$/
* @param string $prefix
* @param string $val
* @return false|string
*/
public function castRelativeToAbsoluteDatetime($prefix, $val)
{
@ -646,6 +734,9 @@ JS;
* the 12th of October 2013. Customize through {@link setDateFormat()}.
*
* @Transform /^(?:(the|a)) date of (?<val>.*)$/
* @param string $prefix
* @param string $val
* @return false|string
*/
public function castRelativeToAbsoluteDate($prefix, $val)
{
@ -696,6 +787,9 @@ JS;
*
* @Then /^the "(?P<name>(?:[^"]|\\")*)" (?P<type>(?:(field|button))) should (?P<negate>(?:(not |)))be disabled/
* @Then /^the (?P<type>(?:(field|button))) "(?P<name>(?:[^"]|\\")*)" should (?P<negate>(?:(not |)))be disabled/
* @param string $name
* @param string $type
* @param string $negate
*/
public function stepFieldShouldBeDisabled($name, $type, $negate)
{
@ -704,7 +798,8 @@ JS;
$element = $page->findField($name);
} else {
$element = $page->find('named', array(
'button', $this->getSession()->getSelectorsHandler()->xpathLiteral($name)
'button',
$this->getMainContext()->getXpathEscaper()->escapeLiteral($name)
));
}
@ -725,6 +820,7 @@ JS;
*
* @Then /^the "(?P<field>(?:[^"]|\\")*)" field should be enabled/
* @Then /^the field "(?P<field>(?:[^"]|\\")*)" should be enabled/
* @param string $field
*/
public function stepFieldShouldBeEnabled($field)
{
@ -745,6 +841,9 @@ JS;
* Example: Given I follow "Select" in the "My Login Form" region
*
* @Given /^I (?:follow|click) "(?P<link>[^"]*)" in the "(?P<region>[^"]*)" region$/
* @param string $link
* @param string $region
* @throws \Exception
*/
public function iFollowInTheRegion($link, $region)
{
@ -767,6 +866,10 @@ JS;
* Example: Given I fill in "Hello" with "World"
*
* @Given /^I fill in "(?P<field>[^"]*)" with "(?P<value>[^"]*)" in the "(?P<region>[^"]*)" region$/
* @param string $field
* @param string $value
* @param string $region
* @throws \Exception
*/
public function iFillinTheRegion($field, $value, $region)
{
@ -793,6 +896,10 @@ JS;
* Example: Given I should not see "My Text" in the "My Login Form" region
*
* @Given /^I should (?P<negate>(?:(not |)))see "(?P<text>[^"]*)" in the "(?P<region>[^"]*)" region$/
* @param string $negate
* @param string $text
* @param string $region
* @throws \Exception
*/
public function iSeeTextInRegion($negate, $text, $region)
{
@ -833,19 +940,23 @@ JS;
* Selects the specified radio button
*
* @Given /^I select the "([^"]*)" radio button$/
* @param string $radioLabel
*/
public function iSelectTheRadioButton($radioLabel)
{
$session = $this->getSession();
$radioButton = $session->getPage()->find('named', array(
'radio', $this->getSession()->getSelectorsHandler()->xpathLiteral($radioLabel)
));
$radioButton = $session->getPage()->find('named', [
'radio',
$this->getMainContext()->getXpathEscaper()->escapeLiteral($radioLabel)
]);
assertNotNull($radioButton);
$session->getDriver()->click($radioButton->getXPath());
}
/**
* @Then /^the "([^"]*)" table should contain "([^"]*)"$/
* @param string $selector
* @param string $text
*/
public function theTableShouldContain($selector, $text)
{
@ -857,6 +968,8 @@ JS;
/**
* @Then /^the "([^"]*)" table should not contain "([^"]*)"$/
* @param string $selector
* @param string $text
*/
public function theTableShouldNotContain($selector, $text)
{
@ -868,6 +981,8 @@ JS;
/**
* @Given /^I click on "([^"]*)" in the "([^"]*)" table$/
* @param string $text
* @param string $selector
*/
public function iClickOnInTheTable($text, $selector)
{
@ -886,11 +1001,12 @@ JS;
* - fieldset[data-name] table
* - table caption
*
* @return Behat\Mink\Element\NodeElement
* @param string $selector
* @return NodeElement
*/
protected function getTable($selector)
{
$selector = $this->getSession()->getSelectorsHandler()->xpathLiteral($selector);
$selector = $this->getMainContext()->getXpathEscaper()->escapeLiteral($selector);
$page = $this->getSession()->getPage();
$candidates = $page->findAll(
'xpath',
@ -914,6 +1030,7 @@ JS;
assertTrue((bool)$candidates, 'Could not find any table elements');
$table = null;
/** @var NodeElement $candidate */
foreach ($candidates as $candidate) {
if (!$table && $candidate->isVisible()) {
$table = $candidate;
@ -929,6 +1046,10 @@ JS;
* Checks the order of two texts.
* Assumptions: the two texts appear in their conjunct parent element once
* @Then /^I should see the text "(?P<textBefore>(?:[^"]|\\")*)" (before|after) the text "(?P<textAfter>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
* @param string $textBefore
* @param string $order
* @param string $textAfter
* @param string $element
*/
public function theTextBeforeAfter($textBefore, $order, $textAfter, $element)
{
@ -955,12 +1076,14 @@ JS;
* Example: Given I wait for 10 seconds until I see the ".css_element" element
*
* @Given /^I wait for (\d+) seconds until I see the "([^"]*)" element$/
**/
* @param int $wait
* @param string $selector
*/
public function iWaitXUntilISee($wait, $selector)
{
$page = $this->getSession()->getPage();
$this->spin(function ($page) use ($page, $selector) {
$this->spin(function () use ($page, $selector) {
$element = $page->find('css', $selector);
if (empty($element)) {
@ -978,11 +1101,12 @@ JS;
* Example: Given I wait until I see the "header .login-form" element
*
* @Given /^I wait until I see the "([^"]*)" element$/
* @param string $selector
*/
public function iWaitUntilISee($selector)
{
$page = $this->getSession()->getPage();
$this->spin(function ($page) use ($page, $selector) {
$this->spin(function () use ($page, $selector) {
$element = $page->find('css', $selector);
if (empty($element)) {
return false;
@ -999,12 +1123,13 @@ JS;
* Example: Given I wait until I see the text "Welcome back, John!"
*
* @Given /^I wait until I see the text "([^"]*)"$/
* @param string $text
*/
public function iWaitUntilISeeText($text)
{
$page = $this->getSession()->getPage();
$session = $this->getSession();
$this->spin(function ($page) use ($page, $session, $text) {
$this->spin(function () use ($page, $session, $text) {
$element = $page->find(
'xpath',
$session->getSelectorsHandler()->selectorToXpath("xpath", ".//*[contains(text(), '$text')]")
@ -1043,6 +1168,8 @@ JS;
* Example: Given I scroll to the "My Date" field
*
* @Given /^I scroll to the "([^"]*)" (field|link|button)$/
* @param string $locator
* @param string $type
*/
public function iScrollToField($locator, $type)
{
@ -1066,6 +1193,7 @@ JS;
* Example: Given I scroll to the ".css_element" element
*
* @Given /^I scroll to the "(?P<locator>(?:[^"]|\\")*)" element$/
* @param string $locator
*/
public function iScrollToElement($locator)
{
@ -1115,12 +1243,34 @@ JS;
}
/**
* We have to catch exceptions and log somehow else otherwise behat falls over
*
* @param Exception $exception
*/
protected function logException($e)
protected function logException(Exception $exception)
{
file_put_contents('php://stderr', 'Exception caught: '.$e);
file_put_contents('php://stderr', 'Exception caught: ' . $exception->getMessage());
}
/**
* Check if a step has a given tag
*
* @param StepScope $event
* @param string $tag
* @return bool
*/
protected function stepHasTag(StepScope $event, $tag)
{
// Check scenario
if ($this->currentScenario && $this->currentScenario->hasTag($tag)) {
return true;
}
// Check feature
$feature = $event->getFeature();
if ($feature && $feature->hasTag($tag)) {
return true;
}
return false;
}
}

View File

@ -2,26 +2,22 @@
namespace SilverStripe\BehatExtension\Context;
use Behat\Behat\Context\BehatContext;
use Behat\Behat\Context\Step;
use Behat\Behat\Event\ScenarioEvent;
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use Behat\Mink\Session;
use SilverStripe\BehatExtension\Utility\TestMailer;
use SilverStripe\Control\Email\Email;
use SilverStripe\Core\Config\Config;
use SilverStripe\Control\Email\Mailer;
use SilverStripe\Core\Injector\Injector;
use Symfony\Component\DomCrawler\Crawler;
// PHPUnit
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
/**
* Context used to define steps related to email sending.
*/
class EmailContext extends BehatContext
class EmailContext implements Context
{
protected $context;
use MainContextAwareTrait;
/**
* @var TestMailer
@ -33,18 +29,6 @@ class EmailContext extends BehatContext
*/
protected $lastMatchedEmail;
/**
* Initializes context.
* Every scenario gets it's own context object.
*
* @param array $parameters context parameters (set them up through behat.yml)
*/
public function __construct(array $parameters)
{
// Initialize your context here
$this->context = $parameters;
}
/**
* Get Mink session from MinkContext
*
@ -58,18 +42,22 @@ class EmailContext extends BehatContext
/**
* @BeforeScenario
* @param BeforeScenarioScope $event
*/
public function before(ScenarioEvent $event)
public function before(BeforeScenarioScope $event)
{
// Also set through the 'supportbehat' extension
// to ensure its available both in CLI execution and the tested browser session
$this->mailer = new TestMailer();
Injector::inst()->registerService($this->mailer, 'SilverStripe\\Control\\Email\\Mailer');
Injector::inst()->registerService($this->mailer, Mailer::class);
Email::config()->update("send_all_emails_to", null);
}
/**
* @Given /^there should (not |)be an email (to|from) "([^"]*)"$/
* @param string $negate
* @param string $direction
* @param string $email
*/
public function thereIsAnEmailFromTo($negate, $direction, $email)
{
@ -86,6 +74,10 @@ class EmailContext extends BehatContext
/**
* @Given /^there should (not |)be an email (to|from) "([^"]*)" titled "([^"]*)"$/
* @param string $negate
* @param string $direction
* @param string $email
* @param string $subject
*/
public function thereIsAnEmailFromToTitled($negate, $direction, $email, $subject)
{
@ -119,6 +111,8 @@ class EmailContext extends BehatContext
* e.g. through 'Given there should be an email to "test@test.com"'.
*
* @Given /^the email should (not |)contain "([^"]*)"$/
* @param string $negate
* @param string $content
*/
public function thereTheEmailContains($negate, $content)
{
@ -148,6 +142,7 @@ class EmailContext extends BehatContext
* e.g. through 'Given there should be an email to "test@test.com"'.
*
* @Given /^the email should contain plain text "([^"]*)"$/
* @param string $content
*/
public function thereTheEmailContainsPlainText($content)
{
@ -165,6 +160,9 @@ class EmailContext extends BehatContext
/**
* @When /^I click on the "([^"]*)" link in the email (to|from) "([^"]*)"$/
* @param string $linkSelector
* @param string $direction
* @param string $email
*/
public function iGoToInTheEmailTo($linkSelector, $direction, $email)
{
@ -179,11 +177,15 @@ class EmailContext extends BehatContext
$link = $linkEl->attr('href');
assertNotNull($link);
return new Step\When(sprintf('I go to "%s"', $link));
$this->getMainContext()->visit($link);
}
/**
* @When /^I click on the "([^"]*)" link in the email (to|from) "([^"]*)" titled "([^"]*)"$/
* @param string $linkSelector
* @param string $direction
* @param string $email
* @param string $title
*/
public function iGoToInTheEmailToTitled($linkSelector, $direction, $email, $title)
{
@ -197,7 +199,7 @@ class EmailContext extends BehatContext
assertNotNull($linkEl);
$link = $linkEl->attr('href');
assertNotNull($link);
return new Step\When(sprintf('I go to "%s"', $link));
$this->getMainContext()->visit($link);
}
/**
@ -205,6 +207,7 @@ class EmailContext extends BehatContext
* e.g. through 'Given there should be an email to "test@test.com"'.
*
* @When /^I click on the "([^"]*)" link in the email"$/
* @param string $linkSelector
*/
public function iGoToInTheEmail($linkSelector)
{
@ -219,7 +222,7 @@ class EmailContext extends BehatContext
$link = $linkEl->attr('href');
assertNotNull($link);
return new Step\When(sprintf('I go to "%s"', $link));
$this->getMainContext()->visit($link);
}
/**
@ -228,7 +231,7 @@ class EmailContext extends BehatContext
public function iClearAllEmails()
{
$this->lastMatchedEmail = null;
return $this->mailer->clearEmails();
$this->mailer->clearEmails();
}
/**
@ -237,6 +240,8 @@ class EmailContext extends BehatContext
* | row2 |
* Assumes an email has been identified by a previous step.
* @Then /^the email should (not |)contain the following data:$/
* @param string $negate
* @param TableNode $table
*/
public function theEmailContainFollowingData($negate, TableNode $table)
{
@ -270,6 +275,8 @@ class EmailContext extends BehatContext
/**
* @Then /^there should (not |)be an email titled "([^"]*)"$/
* @param string $negate
* @param string $subject
*/
public function thereIsAnEmailTitled($negate, $subject)
{
@ -288,6 +295,8 @@ class EmailContext extends BehatContext
/**
* @Then /^the email should (not |)be sent from "([^"]*)"$/
* @param string $negate
* @param string $from
*/
public function theEmailSentFrom($negate, $from)
{
@ -305,6 +314,8 @@ class EmailContext extends BehatContext
/**
* @Then /^the email should (not |)be sent to "([^"]*)"$/
* @param string $negate
* @param string $to
*/
public function theEmailSentTo($negate, $to)
{
@ -325,6 +336,7 @@ class EmailContext extends BehatContext
* e.g. http://localhost/Security/changepassword?m=199&title=reset
* Example: When I click on the http link "changepassword" in the email
* @When /^I click on the http link "([^"]*)" in the email$/
* @param string $httpText
*/
public function iClickOnHttpLinkInEmail($httpText)
{
@ -348,6 +360,6 @@ class EmailContext extends BehatContext
}
assertNotNull($href);
return new Step\When(sprintf('I go to "%s"', $href));
$this->getMainContext()->visit($href);
}
}

View File

@ -2,30 +2,36 @@
namespace SilverStripe\BehatExtension\Context;
use Behat\Behat\Context\BehatContext;
use Behat\Behat\Event\ScenarioEvent;
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\AfterScenarioScope;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Exception;
use InvalidArgumentException;
use SilverStripe\Assets\Folder;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\BehatFixtureFactory;
use SilverStripe\Dev\FixtureBlueprint;
use SilverStripe\Dev\FixtureFactory;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\YamlFixture;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\DataObject;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Security\Group;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
// PHPUnit
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
/**
* Context used to create fixtures in the SilverStripe ORM.
*/
class FixtureContext extends BehatContext
class FixtureContext implements Context
{
use MainContextAwareTrait;
protected $context;
/**
@ -50,14 +56,16 @@ class FixtureContext extends BehatContext
*/
protected $createdAssets = array();
public function __construct(array $parameters)
/**
* FixtureContext constructor.
* @param null $filesPath
*/
public function __construct($filesPath = null)
{
$this->context = $parameters;
if (empty($filesPath)) {
throw new InvalidArgumentException("filesPath is required");
}
public function getSession($name = null)
{
return $this->getMainContext()->getSession($name);
$this->setFilesPath($filesPath);
}
/**
@ -66,14 +74,33 @@ class FixtureContext extends BehatContext
public function getFixtureFactory()
{
if (!$this->fixtureFactory) {
$this->fixtureFactory = Injector::inst()->create(
'SilverStripe\\Dev\\FixtureFactory',
'FixtureContextFactory'
);
$this->fixtureFactory = $this->scaffoldDefaultFixtureFactory();
}
return $this->fixtureFactory;
}
/**
* Build default fixture factory
*
* @return FixtureFactory
*/
protected function scaffoldDefaultFixtureFactory()
{
$fixtureFactory = Injector::inst()->create(BehatFixtureFactory::class);
// Register blueprints
/** @var FixtureBlueprint $blueprint */
$blueprint = Injector::inst()->create(FixtureBlueprint::class, Member::class);
$blueprint->addCallback('beforeCreate', function ($identifier, &$data, &$fixtures) {
if (!isset($data['FirstName'])) {
$data['FirstName'] = $identifier;
}
});
$fixtureFactory->define(Member::class, $blueprint);
return $fixtureFactory;
}
/**
* @param FixtureFactory $factory
*/
@ -100,12 +127,14 @@ class FixtureContext extends BehatContext
/**
* @BeforeScenario @database-defaults
*
* @param BeforeScenarioScope $event
*/
public function beforeDatabaseDefaults(ScenarioEvent $event)
public function beforeDatabaseDefaults(BeforeScenarioScope $event)
{
SapphireTest::empty_temp_db();
DB::get_conn()->quiet();
$dataClasses = ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject');
$dataClasses = ClassInfo::subclassesFor(DataObject::class);
array_shift($dataClasses);
foreach ($dataClasses as $dataClass) {
\singleton($dataClass)->requireDefaultRecords();
@ -114,16 +143,18 @@ class FixtureContext extends BehatContext
/**
* @AfterScenario
* @param AfterScenarioScope $event
*/
public function afterResetDatabase(ScenarioEvent $event)
public function afterResetDatabase(AfterScenarioScope $event)
{
SapphireTest::empty_temp_db();
}
/**
* @AfterScenario
* @param AfterScenarioScope $event
*/
public function afterResetAssets(ScenarioEvent $event)
public function afterResetAssets(AfterScenarioScope $event)
{
$store = $this->getAssetStore();
if (is_array($this->createdAssets)) {
@ -137,18 +168,24 @@ class FixtureContext extends BehatContext
* Example: Given a "page" "Page 1"
*
* @Given /^(?:an|a|the) "([^"]+)" "([^"]+)"$/
* @param string $type
* @param string $id
*/
public function stepCreateRecord($type, $id)
{
$class = $this->convertTypeToClass($type);
$fields = $this->prepareFixture($class, $id);
$this->fixtureFactory->createObject($class, $id, $fields);
$this->getFixtureFactory()->createObject($class, $id, $fields);
}
/**
* Example: Given a "page" "Page 1" has the "content" "My content"
*
* @Given /^(?:an|a|the) "([^"]+)" "([^"]+)" has (?:an|a|the) "(.*)" "(.*)"$/
* @param string $type
* @param string $id
* @param string $field
* @param string $value
*/
public function stepCreateRecordHasField($type, $id, $field, $value)
{
@ -158,14 +195,14 @@ class FixtureContext extends BehatContext
array($field => $value)
);
// We should check if this fixture object already exists - if it does, we update it. If not, we create it
if ($existingFixture = $this->fixtureFactory->get($class, $id)) {
if ($existingFixture = $this->getFixtureFactory()->get($class, $id)) {
// Merge existing data with new data, and create new object to replace existing object
foreach ($fields as $k => $v) {
$existingFixture->$k = $v;
}
$existingFixture->write();
} else {
$this->fixtureFactory->createObject($class, $id, $fields);
$this->getFixtureFactory()->createObject($class, $id, $fields);
}
}
@ -174,6 +211,9 @@ class FixtureContext extends BehatContext
* Example: Given the "page" "Page 1" has "URL"="page-1" and "Content"="my page 1"
*
* @Given /^(?:an|a|the) "([^"]+)" "([^"]+)" (?:with|has) (".*)$/
* @param string $type
* @param string $id
* @param string $data
*/
public function stepCreateRecordWithData($type, $id, $data)
{
@ -189,14 +229,14 @@ class FixtureContext extends BehatContext
);
$fields = $this->prepareFixture($class, $id, $fields);
// We should check if this fixture object already exists - if it does, we update it. If not, we create it
if ($existingFixture = $this->fixtureFactory->get($class, $id)) {
if ($existingFixture = $this->getFixtureFactory()->get($class, $id)) {
// Merge existing data with new data, and create new object to replace existing object
foreach ($fields as $k => $v) {
$existingFixture->$k = $v;
}
$existingFixture->write();
} else {
$this->fixtureFactory->createObject($class, $id, $fields);
$this->getFixtureFactory()->createObject($class, $id, $fields);
}
}
@ -207,6 +247,10 @@ class FixtureContext extends BehatContext
* | My Boolean | bar |
*
* @Given /^(?:an|a|the) "([^"]+)" "([^"]+)" has the following data$/
* @param string $type
* @param string $id
* @param string $null
* @param TableNode $fieldsTable
*/
public function stepCreateRecordWithTable($type, $id, $null, TableNode $fieldsTable)
{
@ -232,6 +276,11 @@ class FixtureContext extends BehatContext
* Note that this change is not published by default
*
* @Given /^(?:an|a|the) "([^"]+)" "([^"]+)" is a ([^\s]*) of (?:an|a|the) "([^"]+)" "([^"]+)"/
* @param string $type
* @param string $id
* @param string $relation
* @param string $relationType
* @param string $relationId
*/
public function stepUpdateRecordRelation($type, $id, $relation, $relationType, $relationId)
{
@ -265,7 +314,7 @@ class FixtureContext extends BehatContext
// already written through $data above
break;
default:
throw new \InvalidArgumentException(sprintf(
throw new InvalidArgumentException(sprintf(
'Invalid relation "%s"',
$relation
));
@ -278,6 +327,10 @@ class FixtureContext extends BehatContext
*
* @example I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1"
* @Given /^I assign (?:an|a|the) "([^"]+)" "([^"]+)" to (?:an|a|the) "([^"]+)" "([^"]+)"$/
* @param string $type
* @param string $value
* @param string $relationType
* @param string $relationId
*/
public function stepIAssignObjToObj($type, $value, $relationType, $relationId)
{
@ -291,6 +344,12 @@ class FixtureContext extends BehatContext
*
* @example I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1" in the "Terms" relation
* @Given /^I assign (?:an|a|the) "([^"]+)" "([^"]+)" to (?:an|a|the) "([^"]+)" "([^"]+)" in the "([^"]+)" relation$/
* @param string $type
* @param string $value
* @param string $relationType
* @param string $relationId
* @param string $relationName
* @throws Exception
*/
public function stepIAssignObjToObjInTheRelation($type, $value, $relationType, $relationId, $relationName)
{
@ -298,28 +357,28 @@ class FixtureContext extends BehatContext
$relationClass = $this->convertTypeToClass($relationType);
// Check if this fixture object already exists - if not, we create it
$relationObj = $this->fixtureFactory->get($relationClass, $relationId);
$relationObj = $this->getFixtureFactory()->get($relationClass, $relationId);
if (!$relationObj) {
$relationObj = $this->fixtureFactory->createObject($relationClass, $relationId);
$relationObj = $this->getFixtureFactory()->createObject($relationClass, $relationId);
}
// Check if there is relationship defined in many_many (includes belongs_many_many)
$manyField = null;
$oneField = null;
if ($relationObj->many_many()) {
$manyField = array_search($class, $relationObj->many_many());
if ($relationObj->manyMany()) {
$manyField = array_search($class, $relationObj->manyMany());
if ($manyField && strlen($relationName) > 0) {
$manyField = $relationName;
}
}
if (empty($manyField) && $relationObj->has_many()) {
$manyField = array_search($class, $relationObj->has_many());
if (empty($manyField) && $relationObj->hasMany(true)) {
$manyField = array_search($class, $relationObj->hasMany());
if ($manyField && strlen($relationName) > 0) {
$manyField = $relationName;
}
}
if (empty($manyField) && $relationObj->has_one()) {
$oneField = array_search($class, $relationObj->has_one());
if (empty($manyField) && $relationObj->hasOne()) {
$oneField = array_search($class, $relationObj->hasOne());
if ($oneField && strlen($relationName) > 0) {
$oneField = $relationName;
}
@ -341,7 +400,7 @@ class FixtureContext extends BehatContext
// Check if the fixture object exists - if not, we create it
$obj = DataObject::get($class)->filter($field, $value)->first();
if (!$obj) {
$obj = $this->fixtureFactory->createObject($class, $value);
$obj = $this->getFixtureFactory()->createObject($class, $value);
}
// If has_many or many_many, add this fixture object to the relation object
// If has_one, set value to the joint field with this fixture object's ID
@ -360,14 +419,17 @@ class FixtureContext extends BehatContext
* Example: Given the "page" "Page 1" is not published
*
* @Given /^(?:an|a|the) "([^"]+)" "([^"]+)" is ([^"]*)$/
* @param string $type
* @param string $id
* @param string $state
*/
public function stepUpdateRecordState($type, $id, $state)
{
$class = $this->convertTypeToClass($type);
/** @var DataObject|Versioned $obj */
$obj = $this->fixtureFactory->get($class, $id);
$obj = $this->getFixtureFactory()->get($class, $id);
if (!$obj) {
throw new \InvalidArgumentException(sprintf(
throw new InvalidArgumentException(sprintf(
'Can not find record "%s" with identifier "%s"',
$type,
$id
@ -390,7 +452,7 @@ class FixtureContext extends BehatContext
$obj->delete();
break;
default:
throw new \InvalidArgumentException(sprintf(
throw new InvalidArgumentException(sprintf(
'Invalid state: "%s"',
$state
));
@ -407,10 +469,12 @@ class FixtureContext extends BehatContext
* Email: member2@test.com
*
* @Given /^there are the following ([^\s]*) records$/
* @param string $dataObject
* @param PyStringNode $string
*/
public function stepThereAreTheFollowingRecords($dataObject, PyStringNode $string)
{
$yaml = array_merge(array($dataObject . ':'), $string->getLines());
$yaml = array_merge(array($dataObject . ':'), $string->getStrings());
$yaml = implode("\n ", $yaml);
// Save fixtures into database
@ -423,15 +487,19 @@ class FixtureContext extends BehatContext
* Example: Given a "member" "Admin" belonging to "Admin Group"
*
* @Given /^(?:an|a|the) "member" "([^"]+)" belonging to "([^"]+)"$/
* @param string $id
* @param string $groupId
*/
public function stepCreateMemberWithGroup($id, $groupId)
{
$group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $groupId);
/** @var Group $group */
$group = $this->getFixtureFactory()->get(Group::class, $groupId);
if (!$group) {
$group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $groupId);
$group = $this->getFixtureFactory()->createObject(Group::class, $groupId);
}
$member = $this->fixtureFactory->createObject('SilverStripe\\Security\\Member', $id);
/** @var Member $member */
$member = $this->getFixtureFactory()->createObject(Member::class, $id);
$member->Groups()->add($group);
}
@ -439,26 +507,30 @@ class FixtureContext extends BehatContext
* Example: Given a "member" "Admin" belonging to "Admin Group" with "Email"="test@test.com"
*
* @Given /^(?:an|a|the) "member" "([^"]+)" belonging to "([^"]+)" with (.*)$/
* @param string $id
* @param string $groupId
* @param string $data
*/
public function stepCreateMemberWithGroupAndData($id, $groupId, $data)
{
$class = 'SilverStripe\\Security\\Member';
preg_match_all(
'/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/',
$data,
$matches
);
$fields = $this->convertFields(
$class,
Member::class,
array_combine($matches['key'], $matches['value'])
);
$group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $groupId);
/** @var Group $group */
$group = $this->getFixtureFactory()->get(Group::class, $groupId);
if (!$group) {
$group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $groupId);
$group = $this->getFixtureFactory()->createObject(Group::class, $groupId);
}
$member = $this->fixtureFactory->createObject($class, $id, $fields);
/** @var Member $member */
$member = $this->getFixtureFactory()->createObject(Member::class, $id, $fields);
$member->Groups()->add($group);
}
@ -466,6 +538,8 @@ class FixtureContext extends BehatContext
* Example: Given a "group" "Admin" with permissions "Access to 'Pages' section" and "Access to 'Files' section"
*
* @Given /^(?:an|a|the) "group" "([^"]+)" (?:with|has) permissions (.*)$/
* @param string $id
* @param string $permissionStr
*/
public function stepCreateGroupWithPermissions($id, $permissionStr)
{
@ -474,9 +548,9 @@ class FixtureContext extends BehatContext
$permissions = $matches[1];
$codes = Permission::get_codes(false);
$group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $id);
$group = $this->getFixtureFactory()->get(Group::class, $id);
if (!$group) {
$group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $id);
$group = $this->getFixtureFactory()->createObject(Group::class, $id);
}
foreach ($permissions as $permission) {
@ -490,7 +564,7 @@ class FixtureContext extends BehatContext
}
}
if (!$found) {
throw new \InvalidArgumentException(sprintf(
throw new InvalidArgumentException(sprintf(
'No permission found for "%s"',
$permission
));
@ -504,22 +578,25 @@ class FixtureContext extends BehatContext
* Example: Given I go to the "page" "My Page"
*
* @Given /^I go to (?:an|a|the) "([^"]+)" "([^"]+)"/
* @param string $type
* @param string $id
*/
public function stepGoToNamedRecord($type, $id)
{
$class = $this->convertTypeToClass($type);
$record = $this->fixtureFactory->get($class, $id);
$record = $this->getFixtureFactory()->get($class, $id);
if (!$record) {
throw new \InvalidArgumentException(sprintf(
throw new InvalidArgumentException(sprintf(
'Cannot resolve reference "%s", no matching fixture found',
$id
));
}
if (!$record->hasMethod('RelativeLink')) {
throw new \InvalidArgumentException('URL for record cannot be determined, missing RelativeLink() method');
throw new InvalidArgumentException('URL for record cannot be determined, missing RelativeLink() method');
}
$link = call_user_func([$record, 'RelativeLink']);
$this->getSession()->visit($this->getMainContext()->locatePath($record->RelativeLink()));
$this->getMainContext()->getSession()->visit($this->getMainContext()->locatePath($link));
}
@ -528,6 +605,8 @@ class FixtureContext extends BehatContext
* Example: There should be a file "assets/Uploads/test.jpg"
*
* @Then /^there should be a ((file|folder) )"([^"]*)"/
* @param string $type
* @param string $path
*/
public function stepThereShouldBeAFileOrFolder($type, $path)
{
@ -540,6 +619,8 @@ class FixtureContext extends BehatContext
* Example: there should be a filename "Uploads/test.jpg" with hash "59de0c841f"
*
* @Then /^there should be a filename "([^"]*)" with hash "([a-fA-Z0-9]+)"/
* @param string $filename
* @param string $hash
*/
public function stepThereShouldBeAFileWithTuple($filename, $hash)
{
@ -552,14 +633,16 @@ class FixtureContext extends BehatContext
* with the notation "=><class>.<identifier>". Example: "=>Page.My Page".
*
* @Transform /^([^"]+)$/
* @param string $string
* @return mixed
*/
public function lookupFixtureReference($string)
{
if (preg_match('/^=>/', $string)) {
list($className, $identifier) = explode('.', preg_replace('/^=>/', '', $string), 2);
$id = $this->fixtureFactory->getId($className, $identifier);
$id = $this->getFixtureFactory()->getId($className, $identifier);
if (!$id) {
throw new \InvalidArgumentException(sprintf(
throw new InvalidArgumentException(sprintf(
'Cannot resolve reference "%s", no matching fixture found',
$string
));
@ -572,12 +655,16 @@ class FixtureContext extends BehatContext
/**
* @Given /^(?:an|a|the) "([^"]*)" "([^"]*)" was (created|last edited) "([^"]*)"$/
* @param string $type
* @param string $id
* @param string $mod
* @param string $time
*/
public function aRecordWasLastEditedRelative($type, $id, $mod, $time)
{
$class = $this->convertTypeToClass($type);
$fields = $this->prepareFixture($class, $id);
$record = $this->fixtureFactory->createObject($class, $id, $fields);
$record = $this->getFixtureFactory()->createObject($class, $id, $fields);
$date = date("Y-m-d H:i:s", strtotime($time));
$table = $record->baseTable();
$field = ($mod == 'created') ? 'Created' : 'LastEdited';
@ -628,7 +715,7 @@ class FixtureContext extends BehatContext
} else {
// Check file exists
if (!file_exists($sourcePath)) {
throw new \InvalidArgumentException(sprintf(
throw new InvalidArgumentException(sprintf(
'Source file for "%s" cannot be found in "%s"',
$relativeTargetPath,
$sourcePath
@ -707,7 +794,7 @@ class FixtureContext extends BehatContext
}
}
throw new \InvalidArgumentException(sprintf(
throw new InvalidArgumentException(sprintf(
'Class "%s" does not exist, or is not a subclass of DataObjet',
$class
));

View File

@ -2,10 +2,9 @@
namespace SilverStripe\BehatExtension\Context\Initializer;
use Behat\Behat\Context\Initializer\InitializerInterface;
use Behat\Behat\Context\ContextInterface;
use SilverStripe\BehatExtension\Context\SilverStripeAwareContextInterface;
use SilverStripe\Core\Injector\Injector;
use Behat\Behat\Context\Initializer\ContextInitializer;
use Behat\Behat\Context\Context;
use SilverStripe\BehatExtension\Context\SilverStripeAwareContext;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\TestSession\TestSessionEnvironment;
@ -24,7 +23,7 @@ use SilverStripe\TestSession\TestSessionEnvironment;
*
* @author Michał Ochman <ochman.d.michal@gmail.com>
*/
class SilverStripeAwareInitializer implements InitializerInterface
class SilverStripeAwareInitializer implements ContextInitializer
{
private $databaseName;
@ -59,12 +58,12 @@ class SilverStripeAwareInitializer implements InitializerInterface
*/
protected $testSessionEnvironment;
protected $regionMap;
/**
* Initializes initializer.
*
* @param string $frameworkPath
*/
public function __construct($frameworkPath)
public function __construct()
{
file_put_contents('php://stdout', 'Bootstrapping' . PHP_EOL);
@ -104,22 +103,24 @@ class SilverStripeAwareInitializer implements InitializerInterface
/**
* Checks if initializer supports provided context.
*
* @param ContextInterface $context
*
* @param Context $context
* @return Boolean
*/
public function supports(ContextInterface $context)
public function supports(Context $context)
{
return $context instanceof SilverStripeAwareContextInterface;
return $context instanceof SilverStripeAwareContext;
}
/**
* Initializes provided context.
*
* @param ContextInterface $context
* @param Context $context
*/
public function initialize(ContextInterface $context)
public function initializeContext(Context $context)
{
if (! $context instanceof SilverStripeAwareContext) {
return;
}
$context->setDatabase($this->databaseName);
$context->setAjaxSteps($this->ajaxSteps);
$context->setAjaxTimeout($this->ajaxTimeout);

View File

@ -0,0 +1,181 @@
<?php
namespace SilverStripe\BehatExtension\Context;
use Behat\Behat\Context\Context;
use Behat\Mink\Element\NodeElement;
use SilverStripe\Security\Group;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
/**
* LoginContext
*
* Context used to define steps related to login and logout functionality
*/
class LoginContext implements Context
{
use MainContextAwareTrait;
/**
* @Given /^I am logged in$/
*/
public function stepIAmLoggedIn()
{
$c = $this->getMainContext();
$adminUrl = $c->joinUrlParts($c->getBaseUrl(), $c->getAdminUrl());
$loginUrl = $c->joinUrlParts($c->getBaseUrl(), $c->getLoginUrl());
$this->getMainContext()->getSession()->visit($adminUrl);
if (0 == strpos($this->getMainContext()->getSession()->getCurrentUrl(), $loginUrl)) {
$this->stepILogInWith('admin', 'password');
assertStringStartsWith($adminUrl, $this->getMainContext()->getSession()->getCurrentUrl());
}
}
/**
* Creates a member in a group with the correct permissions.
* Example: Given I am logged in with "ADMIN" permissions
*
* @Given /^I am logged in with "([^"]*)" permissions$/
* @param string $permCode
*/
public function iAmLoggedInWithPermissions($permCode)
{
$email = "{$permCode}@example.org";
$password = 'Secret!123';
$this->generateMemberWithPermission($email, $password, $permCode);
$this->stepILogInWith($email, $password);
}
/**
* @Given /^I am not logged in$/
*/
public function stepIAmNotLoggedIn()
{
$c = $this->getMainContext();
$this->getMainContext()->getSession()->visit($c->joinUrlParts($c->getBaseUrl(), 'Security/logout'));
}
/**
* @When /^I log in with "(?<username>[^"]*)" and "(?<password>[^"]*)"$/
* @param string $email
* @param string $password
*/
public function stepILogInWith($email, $password)
{
$c = $this->getMainContext();
$loginUrl = $c->joinUrlParts($c->getBaseUrl(), $c->getLoginUrl());
$this->getMainContext()->getSession()->visit($loginUrl);
$page = $this->getMainContext()->getSession()->getPage();
$forms = $page->findAll('xpath', '//form[contains(@action, "Security/LoginForm")]');
assertNotNull($forms, 'Login form not found');
// Try to find visible forms again on login page.
$visibleForm = null;
/** @var NodeElement $form */
foreach ($forms as $form) {
if ($form->isVisible() && $form->find('css', '[name=Email]')) {
$visibleForm = $form;
}
}
assertNotNull($visibleForm, 'Could not find login form');
$emailField = $visibleForm->find('css', '[name=Email]');
$passwordField = $visibleForm->find('css', '[name=Password]');
$submitButton = $visibleForm->find('css', '[type=submit]');
$securityID = $visibleForm->find('css', '[name=SecurityID]');
assertNotNull($emailField, 'Email field on login form not found');
assertNotNull($passwordField, 'Password field on login form not found');
assertNotNull($submitButton, 'Submit button on login form not found');
assertNotNull($securityID, 'CSRF token not found');
$emailField->setValue($email);
$passwordField->setValue($password);
$submitButton->press();
}
/**
* @Given /^I should see a log-in form$/
*/
public function stepIShouldSeeALogInForm()
{
$page = $this->getMainContext()->getSession()->getPage();
$loginForm = $page->find('css', '#MemberLoginForm_LoginForm');
assertNotNull($loginForm, 'I should see a log-in form');
}
/**
* @Then /^I will see a "([^"]*)" log-in message$/
* @param string $type
*/
public function stepIWillSeeALogInMessage($type)
{
$page = $this->getMainContext()->getSession()->getPage();
$message = $page->find('css', sprintf('.message.%s', $type));
assertNotNull($message, sprintf('%s message not found.', $type));
}
/**
* @Then /^the password for "([^"]*)" should be "([^"]*)"$/
* @skipUpgrade
* @param string $id
* @param string $password
*/
public function stepPasswordForEmailShouldBe($id, $password)
{
/** @var Member $member */
$member = Member::get()->filter('Email', $id)->First();
assertNotNull($member);
assertTrue($member->checkPassword($password)->isValid());
}
/**
* Get or generate a member with the given permission code
*
* @param string $email
* @param string $password
* @param string $permCode
* @return Member
*/
protected function generateMemberWithPermission($email, $password, $permCode)
{
// Get or create group
$group = Group::get()->filter('Title', "$permCode group")->first();
if (!$group) {
$group = Group::create();
}
$group->Title = "$permCode group";
$group->write();
// Get or create permission
$permission = Permission::create();
$permission->Code = $permCode;
$permission->write();
$group->Permissions()->add($permission);
// Get or create member
$member = Member::get()->filter('Email', $email)->first();
if (!$member) {
$member = Member::create();
}
// make sure any validation for password is skipped, since we're not testing complexity here
$validator = Member::password_validator();
Member::set_password_validator(null);
$member->FirstName = $permCode;
$member->Surname = "User";
$member->Email = $email;
$member->PasswordEncryption = "none";
$member->changePassword($password);
$member->write();
$group->Members()->add($member);
Member::set_password_validator($validator);
return $member;
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace SilverStripe\BehatExtension\Context;
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
/**
* Represents a behat context which is aware of a main {@see SilverStripeContext} context.
*
* Nested contexts are bootstrapped by SilverStripeContext::gatherContexts()
*/
trait MainContextAwareTrait
{
/**
* @var SilverStripeContext
*/
protected $mainContext;
/**
* Get the main context
*
* @return SilverStripeContext
*/
public function getMainContext()
{
return $this->mainContext;
}
/**
* @param SilverStripeContext $mainContext
* @return $this
*/
public function setMainContext($mainContext)
{
$this->mainContext = $mainContext;
return $this;
}
/**
* Helper method to detect the main context
*
* @BeforeScenario
* @param BeforeScenarioScope $scope
*/
public function detectMainContext(BeforeScenarioScope $scope)
{
$environment = $scope->getEnvironment();
if (! $environment instanceof InitializedContextEnvironment) {
throw new \LogicException("No context available for this environment");
}
$contexts = $environment->getContexts();
foreach ($contexts as $context) {
if ($context instanceof SilverStripeContext) {
$this->setMainContext($context);
return;
}
}
throw new \LogicException("No SilverStripeContext is configured");
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace SilverStripe\BehatExtension\Context;
trait RetryableContextTrait
{
/**
* Invoke callback for a non-empty result with a given timeout
*
* @param callable $callback
* @param int $timeout Number of seconds to retry for
* @return mixed Result of invoking $try, or null if timed out
*/
protected function retryUntil($callback, $timeout = 3)
{
do {
$result = $callback();
if ($result) {
return $result;
}
sleep(1);
} while (--$timeout >= 0);
return null;
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace SilverStripe\BehatExtension\Context;
/*
* This file is part of the Behat/SilverStripeExtension
*
* (c) Michał Ochman <ochman.d.michal@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
use Behat\MinkExtension\Context\MinkAwareContext;
/**
* SilverStripe aware interface for contexts.
*
* @author Michał Ochman <ochman.d.michal@gmail.com>
*/
interface SilverStripeAwareContext extends MinkAwareContext
{
/**
* Sets SilverStripe instance.
*
* @param string $databaseName Temp database name
*/
public function setDatabase($databaseName);
/**
* Marks steps as AJAX steps for special treatment
*
* @param array $ajaxSteps Array of step name parts to match
*/
public function setAjaxSteps($ajaxSteps);
/**
* Set timeout in millisceonds
*
* @param int $ajaxTimeout
*/
public function setAjaxTimeout($ajaxTimeout);
/**
* Set admin url
*
* @param string $adminUrl
*/
public function setAdminUrl($adminUrl);
/**
* Set login url
*
* @param string $loginUrl
*/
public function setLoginUrl($loginUrl);
/**
* Set path to screenshots dir
*
* @param string $screenshotPath
*/
public function setScreenshotPath($screenshotPath);
/**
* I have no idea
*
* @param $regionMap
*/
public function setRegionMap($regionMap);
}

View File

@ -2,29 +2,32 @@
namespace SilverStripe\BehatExtension\Context;
use Behat\Behat\Context\Step;
use Behat\Behat\Event\ScenarioEvent;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Selector\Xpath\Escaper;
use Behat\MinkExtension\Context\MinkContext;
use Behat\Mink\Driver\GoutteDriver;
use Behat\Mink\Driver\Selenium2Driver;
use Behat\Mink\Exception\UnsupportedDriverActionException;
use Behat\Mink\Exception\ElementNotFoundException;
use InvalidArgumentException;
use SilverStripe\BehatExtension\Context\SilverStripeAwareContextInterface;
use Symfony\Component\Yaml\Yaml;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Resettable;
use SilverStripe\ORM\DataObject;
use SilverStripe\TestSession\TestSessionEnvironment;
// Mink etc.
require_once 'vendor/autoload.php';
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
/**
* SilverStripeContext
*
* Generic context wrapper used as a base for Behat FeatureContext.
*
* The default context for each module should extend this and be named `FeatureContext`
* under the standard module namespace.
*
* @link http://behat.org/en/latest/user_guide/context.html
*/
class SilverStripeContext extends MinkContext implements SilverStripeAwareContextInterface
abstract class SilverStripeContext extends MinkContext implements SilverStripeAwareContext
{
protected $databaseName;
@ -32,7 +35,7 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
* @var array Partial string match for step names
* that are considered to trigger Ajax request in the CMS,
* and hence need special timeout handling.
* @see \SilverStripe\BehatExtension\Context\BasicContext->handleAjaxBeforeStep().
* @see \SilverStripe\BehatExtension\Context\BasicContextAwareTrait->handleAjaxBeforeStep().
*/
protected $ajaxSteps;
@ -58,10 +61,19 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
*/
protected $screenshotPath;
protected $context;
/**
* @var TestSessionEnvironment
*/
protected $testSessionEnvironment;
protected $regionMap;
/**
* XPath escaper
*
* @var Escaper
*/
protected $xpathEscaper;
/**
* Initializes context.
@ -69,11 +81,27 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
*
* @param array $parameters context parameters (set them up through behat.yml)
*/
public function __construct(array $parameters)
public function __construct(array $parameters = null)
{
if (!preg_match('/\\FeatureContext$/', get_class($this))) {
throw new InvalidArgumentException(
'Subclasses of SilverStripeContext must be named FeatureContext. Found "' . get_class($this) . '""'
);
}
// Initialize your context here
$this->context = $parameters;
$this->testSessionEnvironment = new TestSessionEnvironment();
$this->xpathEscaper = new Escaper();
$this->testSessionEnvironment = TestSessionEnvironment::singleton();
}
/**
* Get xpath escaper
*
* @return Escaper
*/
public function getXpathEscaper()
{
return $this->xpathEscaper;
}
public function setDatabase($databaseName)
@ -144,12 +172,13 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
}
/**
* Returns MinkElement based off region defined in .yml file.
* Returns NodeElement based off region defined in .yml file.
* Also supports direct CSS selectors and regions identified by a "data-title" attribute.
* When using the "data-title" attribute, ensure not to include double quotes.
*
* @param string $region Region name or CSS selector
* @return MinkElement
* @return NodeElement
* @throws ElementNotFoundException
*/
public function getRegionObj($region)
{
@ -158,12 +187,12 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
$regionObj = $this->getSession()->getPage()->find(
'css',
// Escape CSS selector
(false !== strpos($region, "'")) ? str_replace("'", "\'", $region) : $region
(false !== strpos($region, "'")) ? str_replace("'", "\\'", $region) : $region
);
if ($regionObj) {
return $regionObj;
}
} catch (\Symfony\Component\CssSelector\Exception\SyntaxErrorException $e) {
} catch (SyntaxErrorException $e) {
// fall through to next case
}
@ -189,7 +218,7 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
}
$regionObj = $this->getSession()->getPage()->find('css', $region);
if (!$regionObj) {
throw new ElementNotFoundException("Cannot find the specified region on the page");
throw new ElementNotFoundException($this->getSession(), "Cannot find the specified region on the page");
}
return $regionObj;
@ -197,8 +226,9 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
/**
* @BeforeScenario
* @param BeforeScenarioScope $event
*/
public function before(ScenarioEvent $event)
public function before(BeforeScenarioScope $event)
{
if (!isset($this->databaseName)) {
throw new \LogicException(
@ -231,6 +261,14 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
} else {
$this->getSession()->resizeWindow(1024, 768);
}
// Reset everything
foreach (ClassInfo::implementorsOf(Resettable::class) as $class) {
$class::reset();
}
DataObject::flush_and_destroy_cache();
DataObject::reset();
SiteTree::reset();
}
/**
@ -319,14 +357,14 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
* Forward slash usages are normalised to one between parts.
* This method takes variable number of parameters.
*
* @param $...
* @param string $part,...
* @return string
* @throws \InvalidArgumentException
* @throws InvalidArgumentException
*/
public function joinUrlParts()
public function joinUrlParts($part = null)
{
if (0 === func_num_args()) {
throw new \InvalidArgumentException('Need at least one argument');
throw new InvalidArgumentException('Need at least one argument');
}
$parts = func_get_args();
@ -341,44 +379,35 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
public function canIntercept()
{
$driver = $this->getSession()->getDriver();
if ($driver instanceof GoutteDriver) {
return true;
} else {
if ($driver instanceof Selenium2Driver) {
return false;
}
}
throw new UnsupportedDriverActionException('You need to tag the scenario with "@mink:goutte" or
"@mink:symfony". Intercepting the redirections is not supported by %s', $driver);
}
/**
* @Given /^(.*) without redirection$/
*/
public function theRedirectionsAreIntercepted($step)
{
if ($this->canIntercept()) {
$this->getSession()->getDriver()->getClient()->followRedirects(false);
}
return new Step\Given($step);
throw new UnsupportedDriverActionException(
'You need to tag the scenario with "@mink:symfony". Intercepting the redirections is not supported by %s',
get_class($driver)
);
}
/**
* Fills in form field with specified id|name|label|value.
* Overwritten to select the first *visible* element, see https://github.com/Behat/Mink/issues/311
*
* @param string $field
* @param string $value
* @throws ElementNotFoundException
*/
public function fillField($field, $value)
{
$value = $this->fixStepArgument($value);
$fields = $this->getSession()->getPage()->findAll('named', array(
'field', $this->getSession()->getSelectorsHandler()->xpathLiteral($field)
$nodes = $this->getSession()->getPage()->findAll('named', array(
'field', $this->getXpathEscaper()->escapeLiteral($field)
));
if ($fields) {
foreach ($fields as $f) {
if ($f->isVisible()) {
$f->setValue($value);
if ($nodes) {
/** @var NodeElement $node */
foreach ($nodes as $node) {
if ($node->isVisible()) {
$node->setValue($value);
return;
}
}
@ -394,17 +423,21 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
/**
* Overwritten to click the first *visable* link the DOM.
*
* @param string $link
* @throws ElementNotFoundException
*/
public function clickLink($link)
{
$link = $this->fixStepArgument($link);
$links = $this->getSession()->getPage()->findAll('named', array(
'link', $this->getSession()->getSelectorsHandler()->xpathLiteral($link)
$nodes = $this->getSession()->getPage()->findAll('named', array(
'link', $this->getXpathEscaper()->escapeLiteral($link)
));
if ($links) {
foreach ($links as $l) {
if ($l->isVisible()) {
$l->click();
if ($nodes) {
/** @var NodeElement $node */
foreach ($nodes as $node) {
if ($node->isVisible()) {
$node->click();
return;
}
}
@ -424,6 +457,7 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
* Example: Given the current date is "2009-10-31"
*
* @Given /^the current date is "([^"]*)"$/
* @param string $date
*/
public function givenTheCurrentDateIs($date)
{
@ -448,6 +482,7 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
* Example: Given the current time is "20:31:50"
*
* @Given /^the current time is "([^"]*)"$/
* @param string $time
*/
public function givenTheCurrentTimeIs($time)
{
@ -469,6 +504,8 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
* Selects option in select field with specified id|name|label|value.
*
* @override /^(?:|I )select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
* @param string $select
* @param string $option
*/
public function selectOption($select, $option)
{
@ -492,6 +529,9 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
* overridden by javascript libraries, and thus hide the element.
*
* @When /^(?:|I )select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)" with javascript$/
* @param string $select
* @param string $option
* @throws ElementNotFoundException
*/
public function selectOptionWithJavascript($select, $option)
{
@ -507,7 +547,7 @@ class SilverStripeContext extends MinkContext implements SilverStripeAwareContex
// Find option
$opt = $field->find('named', array(
'option', $this->getSession()->getSelectorsHandler()->xpathLiteral($option)
'option', $this->getXpathEscaper()->escapeLiteral($option)
));
if (null === $opt) {
throw new ElementNotFoundException($this->getSession(), 'select option', 'value|text', $option);

View File

@ -0,0 +1,28 @@
<?php
namespace SilverStripe\BehatExtension\Controllers;
use InvalidArgumentException;
use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader;
trait ModuleCommandTrait
{
/**
* Find target module being tested
*
* @param string $name
* @return Module
*/
protected function getModule($name)
{
if (strpos($name, '@') === 0) {
$name = substr($name, 1);
}
$module = ModuleLoader::instance()->getManifest()->getModule($name);
if (!$module) {
throw new InvalidArgumentException("No module $name installed");
}
return $module;
}
}

View File

@ -0,0 +1,288 @@
<?php
/*
* This file is part of the Behat Testwork.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SilverStripe\BehatExtension\Controllers;
use Behat\Testwork\Cli\Controller;
use Behat\Testwork\Suite\SuiteBootstrapper;
use Behat\Testwork\Suite\SuiteRepository;
use Exception;
use SilverStripe\Core\Manifest\Module;
use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\Output;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Yaml\Yaml;
/**
* Initialises module test environment.
*
* Replaces:
* @see \Behat\Testwork\Suite\Cli\InitializationController
*/
class ModuleInitialisationController implements Controller
{
use ModuleCommandTrait;
/**
* @var Container
*/
protected $container;
/**
* @var SuiteRepository
*/
private $repository;
/**
* @var SuiteBootstrapper
*/
private $bootstrapper;
/**
* Initializes controller.
*
* @param ContainerInterface $container
* @param SuiteRepository $repository
* @param SuiteBootstrapper $bootstrapper
*/
public function __construct(
ContainerInterface $container,
SuiteRepository $repository,
SuiteBootstrapper $bootstrapper
) {
$this->container = $container;
$this->repository = $repository;
$this->bootstrapper = $bootstrapper;
}
/**
* {@inheritdoc}
*/
public function configure(Command $command)
{
$command->addOption(
'--init',
null,
InputOption::VALUE_NONE,
'Initialize all registered test suites.'
);
$command->addOption(
'--namespace',
null,
InputOption::VALUE_REQUIRED,
'Set namespace for fixture'
);
}
/**
* {@inheritdoc}
*/
public function execute(InputInterface $input, OutputInterface $output)
{
if (!$input->getOption('init')) {
return null;
}
// If module not specified, bootstrap via legacy behaviour
if (!$input->hasArgument('module')) {
return $this->baseExecute($output);
}
if (!$input->hasOption('namespace')) {
throw new \BadMethodCallException(
"--namespace is required if --init is invoked with a module "
. "This should just be your root Vendor\\Module namespace (e.g. 'SilverStripe\\CMS')"
);
}
// Get module
$moduleName = $input->getArgument('module');
$module = $this->getModule($moduleName);
$namespaceRoot = $input->getOption('namespace');
// Init components
$this->initFeaturesPath($output, $module);
$this->initClassPath($output, $module, $namespaceRoot);
$this->initConfig($output, $module, $namespaceRoot);
return 0;
}
/**
* @param OutputInterface $output
* @return int
*/
protected function baseExecute(OutputInterface $output)
{
$suites = $this->repository->getSuites();
$this->bootstrapper->bootstrapSuites($suites);
$output->write(PHP_EOL);
return 0;
}
protected function initFeaturesPath(OutputInterface $output, Module $module)
{
// Create feature_path
$features = $this->container->getParameter('silverstripe_extension.context.features_path');
$fullPath = $module->getResourcePath($features);
if (is_dir($fullPath)) {
return;
}
mkdir($fullPath, 0777, true);
$output->writeln(
"<info>{$fullPath}</info> - <comment>place your *.feature files here</comment>"
);
// Create dummy feature
$featureContent = ArrayData::create([])
->renderWith(__DIR__.'/../../templates/SkeletonFeature.ss');
file_put_contents($fullPath.'/placeholder.feature', $featureContent);
}
/**
* Init class_path
*
* @param OutputInterface $output
* @param Module $module
* @param string $namespaceRoot
* @throws Exception
*/
protected function initClassPath(OutputInterface $output, Module $module, $namespaceRoot)
{
$classesPath = $this->container->getParameter('silverstripe_extension.context.class_path');
$dirPath = $module->getResourcePath($classesPath);
if (!is_dir($dirPath)) {
mkdir($dirPath, 0777, true);
}
// Scaffold base context file
$classPath = "{$dirPath}/FeatureContext.php";
if (is_file($classPath)) {
return;
}
// Build class name
$fullNamespace = $this->getFixtureNamespace($namespaceRoot);
$class = $this->getFixtureClass($namespaceRoot);
// Render class
$obj = ArrayData::create([
'Namespace' => $fullNamespace,
'ClassName' => $class,
]);
$classContent = $obj->renderWith(__DIR__.'/../../templates/FeatureContext.ss');
file_put_contents($classPath, $classContent);
// Log
$output->writeln(
"<info>{$classPath}</info> - <comment>place your feature related code here</comment>"
);
// Add to composer json
$composerFile = $module->getResourcePath('composer.json');
if (!file_exists($composerFile)) {
return;
}
// Add autoload directive to composer
$composerData = json_decode(file_get_contents($composerFile), true);
if (json_last_error()) {
throw new Exception(json_last_error_msg());
}
if (!isset($composerData['autoload'])) {
$composerData['autoload'] = [];
}
if (!isset($composerData['autoload']['psr-4'])) {
$composerData['autoload']['psr-4'] = [];
}
$composerData['autoload']['psr-4']["{$fullNamespace}\\"] = $classesPath;
file_put_contents(
$composerFile,
json_encode($composerData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
$output->writeln(
"<info>{$composerFile}</info> - <comment>psr-4 autload for this class added</comment>"
);
}
/**
* Get fixture class name
*
* @param string $namespaceRoot
* @return string
*/
protected function getFixtureClass($namespaceRoot)
{
$fullNamespace = $this->getFixtureNamespace($namespaceRoot);
return $fullNamespace . '\FeatureContext';
}
/**
* @param string $namespaceRoot
* @return string
*/
protected function getFixtureNamespace($namespaceRoot)
{
$namespaceSuffix = $this->container->getParameter('silverstripe_extension.context.namespace_suffix');
return trim($namespaceRoot, '/\\') . '\\' . $namespaceSuffix;
}
/**
* Init config file behat.yml
*
* @param OutputInterface $output
* @param Module $module
* @param string $namespaceRoot
*/
protected function initConfig($output, $module, $namespaceRoot)
{
$configPath = $module->getResourcePath('behat.yml');
if (file_exists($configPath)) {
return;
}
$class = $this->getFixtureClass($namespaceRoot);
// load config from yml
$features = $this->container->getParameter('silverstripe_extension.context.features_path');
$data = Yaml::parse(file_get_contents(__DIR__.'/../../templates/config-base.yml'));
$shortname = $module->getShortName();
$data['default']['suites'][$shortname] = [
'paths' => [
"%paths.modules.{$shortname}%/{$features}",
],
'contexts' => [
$class,
\SilverStripe\Framework\Tests\Behaviour\CmsFormsContext::class,
\SilverStripe\Framework\Tests\Behaviour\CmsUiContext::class,
\SilverStripe\BehatExtension\Context\BasicContext::class,
\SilverStripe\BehatExtension\Context\EmailContext::class,
\SilverStripe\BehatExtension\Context\LoginContext::class,
[
\SilverStripe\BehatExtension\Context\FixtureContext::class => [
'%paths.modules.framework%/tests/behat/features/files/'
]
]
]
];
file_put_contents($configPath, Yaml::dump($data, 99999999, 2));
$output->writeln(
"<info>{$configPath}</info> - <comment>default behat.yml created</comment>"
);
}
}

View File

@ -0,0 +1,171 @@
<?php
namespace SilverStripe\BehatExtension\Controllers;
use Behat\Testwork\Cli\Controller;
use Behat\Testwork\Suite\Cli\SuiteController;
use Behat\Testwork\Suite\ServiceContainer\SuiteExtension;
use Behat\Testwork\Suite\SuiteRegistry;
use Exception;
use SilverStripe\Core\Manifest\Module;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Parser;
/**
* Locates test suite configuration based on module name.
*
* @see SuiteController for similar core behat controller
*/
class ModuleSuiteLocator implements Controller
{
use ModuleCommandTrait;
/**
* @var Container
*/
protected $container;
/**
* @var SuiteRegistry
*/
protected $registry;
/**
* Cache of configured suites
*
* @see SuiteExtension Which registers these
* @var array
*/
private $suiteConfigurations = array();
/**
* Init suite locator
*
* @param ContainerInterface $container
* @param SuiteRegistry $registry
*/
public function __construct(
ContainerInterface $container,
SuiteRegistry $registry
) {
$this->container = $container;
$this->registry = $registry;
$this->suiteConfigurations = $container->getParameter('suite.configurations');
}
/**
* Configures command to be able to process it later.
*
* @param Command $command
*/
public function configure(Command $command)
{
$command->addArgument(
'module',
InputArgument::OPTIONAL,
"Specific module suite to load. "
. "Must be in @modulename format. Supports @vendor/name syntax for vendor installed modules. "
. "Ensure that a modulename/behat.yml exists containing a behat suite of the same name."
);
}
/**
* Processes data from container and console input.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @throws \RuntimeException
* @return null
*/
public function execute(InputInterface $input, OutputInterface $output)
{
if (!$input->hasArgument('module')) {
return null;
}
// Don't register config if init
if ($input->getOption('init')) {
return;
}
// Get module
$moduleName = $input->getArgument('module');
$module = $this->getModule($moduleName);
// Suite name always omits vendor
$suiteName = $module->getShortName();
// If suite is already configured in the root, switch to it and return
if (isset($this->suiteConfigurations[$suiteName])) {
$config = $this->suiteConfigurations[$suiteName];
$this->registry->registerSuiteConfiguration(
$suiteName,
$config['type'],
$config['settings']
);
return null;
}
// Suite doesn't exist, so load dynamically from nested `behat.yml`
$config = $this->loadSuiteConfiguration($suiteName, $module);
$this->registry->registerSuiteConfiguration(
$suiteName,
$config['type'],
$config['settings']
);
return null;
}
/**
* Get behat.yml configured for this module
*
* @param Module $module
* @return string Path to config
*/
protected function findModuleConfig(Module $module)
{
$pathSuffix = $this->container->getParameter('silverstripe_extension.context.features_path');
$path = $module->getPath();
// Find all candidate paths
foreach ([ "{$path}/", "{$path}/{$pathSuffix}"] as $parent) {
foreach ([$parent.'behat.yml', $parent.'.behat.yml'] as $candidate) {
if (file_exists($candidate)) {
return $candidate;
}
}
}
throw new \InvalidArgumentException("No behat.yml found for module " . $module->getName());
}
/**
* Load configuration dynamically from yml
*
* @param string $suite Suite name
* @param Module $module
* @return array
* @throws Exception
*/
protected function loadSuiteConfiguration($suite, Module $module)
{
$path = $this->findModuleConfig($module);
$yamlParser = new Parser();
$config = $yamlParser->parse(file_get_contents($path));
if (empty($config['default']['suites'][$suite])) {
throw new Exception("Path {$path} does not contain default.suites.{$suite} config");
}
$suiteConfig = $config['default']['suites'][$suite];
// Resolve variables
$resolvedConfig = $this->container->getParameterBag()->resolveValue($suiteConfig);
return [
'type' => null, // @todo figure out what this is for
'settings' => $resolvedConfig,
];
}
}

164
src/Extension.php Normal file
View File

@ -0,0 +1,164 @@
<?php
namespace SilverStripe\BehatExtension;
use Behat\Testwork\Cli\ServiceContainer\CliExtension;
use Behat\Testwork\Suite\Cli\InitializationController;
use Behat\Testwork\Suite\ServiceContainer\SuiteExtension;
use SilverStripe\BehatExtension\Controllers\ModuleInitialisationController;
use SilverStripe\BehatExtension\Controllers\ModuleSuiteLocator;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Behat\Testwork\ServiceContainer\ExtensionManager;
use Behat\Testwork\ServiceContainer\Extension as ExtensionInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/*
* This file is part of the SilverStripe\BehatExtension
*
* (c) Michał Ochman <ochman.d.michal@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* SilverStripe extension for Behat class.
*
* Configured by adding `SilverStripe\BehatExtension\Extension` to your behat.yml
*
* @author Michał Ochman <ochman.d.michal@gmail.com>
*/
class Extension implements ExtensionInterface
{
/**
* Extension configuration ID.
*/
const SILVERSTRIPE_ID = 'silverstripe_extension';
/**
* {@inheritDoc}
*/
public function getConfigKey()
{
return self::SILVERSTRIPE_ID;
}
public function initialize(ExtensionManager $extensionManager)
{
// PHPUnit
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
}
public function load(ContainerBuilder $container, array $config)
{
// Load yml config
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../config'));
$loader->load('silverstripe.yml');
// Add CLI substitutions
$this->loadSuiteLocator($container);
$this->loadBootstrapController($container);
// Set various paths
$container->setParameter('silverstripe_extension.admin_url', $config['admin_url']);
$container->setParameter('silverstripe_extension.login_url', $config['login_url']);
$container->setParameter('silverstripe_extension.screenshot_path', $config['screenshot_path']);
$container->setParameter('silverstripe_extension.ajax_timeout', $config['ajax_timeout']);
if (isset($config['ajax_steps'])) {
$container->setParameter('silverstripe_extension.ajax_steps', $config['ajax_steps']);
}
if (isset($config['region_map'])) {
$container->setParameter('silverstripe_extension.region_map', $config['region_map']);
}
$container->setParameter('silverstripe_extension.bootstrap_file', $config['bootstrap_file']);
}
/**
* {@inheritDoc}
*/
public function process(ContainerBuilder $container)
{
$corePass = new Compiler\CoreInitializationPass();
$corePass->process($container);
}
public function configure(ArrayNodeDefinition $builder)
{
$builder->
children()->
scalarNode('screenshot_path')->
defaultNull()->
end()->
arrayNode('region_map')->
useAttributeAsKey('key')->
prototype('variable')->end()->
end()->
scalarNode('admin_url')->
defaultValue('/admin/')->
end()->
scalarNode('login_url')->
defaultValue('/Security/login')->
end()->
scalarNode('ajax_timeout')->
defaultValue(5000)->
end()->
scalarNode('bootstrap_file')->
defaultNull()->
end()->
arrayNode('ajax_steps')->
defaultValue(array(
'go to',
'follow',
'press',
'click',
'submit'
))->
prototype('scalar')->
end()->
end()->
end();
}
/**
* Loads module suite locator.
* This is responsible for bootstrapping the module config
* for running tests.
*
* @param ContainerBuilder $container
*/
protected function loadSuiteLocator(ContainerBuilder $container)
{
$definition = new Definition(ModuleSuiteLocator::class, [
$container,
new Reference(SuiteExtension::REGISTRY_ID)
]);
$definition->addTag(CliExtension::CONTROLLER_TAG, ['priority' => 9999]);
$container->setDefinition(CliExtension::CONTROLLER_TAG . '.sslocator', $definition);
}
/**
* Loads suite bootstrap controller.
* This is responsible for invoking --init commands for modules.
* Replaces the core behat InitializationController
*
* @see InitializationController
* @param ContainerBuilder $container
*/
protected function loadBootstrapController(ContainerBuilder $container)
{
$definition = new Definition(ModuleInitialisationController::class, [
$container,
new Reference(SuiteExtension::REGISTRY_ID),
new Reference(SuiteExtension::BOOTSTRAPPER_ID)
]);
$definition->addTag(CliExtension::CONTROLLER_TAG, ['priority' => 900]);
$container->setDefinition(CliExtension::CONTROLLER_TAG . '.initialization', $definition);
}
}

23
src/MinkExtension.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\BehatExtension;
use Behat\MinkExtension\ServiceContainer\MinkExtension as BaseMinkExtension;
use SilverStripe\BehatExtension\Compiler\MinkExtensionBaseUrlPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Subclass the main extension in order to get a say in the config compilation.
* We need to intercept setting the base_url to auto-detect it from SilverStripe configuration.
*
* Configured by adding `SilverStripe\BehatExtension\MinkExtension` to your behat.yml
*/
class MinkExtension extends BaseMinkExtension
{
public function process(ContainerBuilder $container)
{
parent::process($container);
$urlPass = new MinkExtensionBaseUrlPass();
$urlPass->process($container);
}
}

View File

@ -1,107 +0,0 @@
<?php
namespace SilverStripe\BehatExtension\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Behat\SilverStripe container compilation pass.
* Passes Base URL available in MinkExtension config.
* Used for the {@link \SilverStripe\BehatExtension\MinkExtension} subclass.
*
* @author Michał Ochman <ochman.d.michal@gmail.com>
*/
class MinkExtensionBaseUrlPass implements CompilerPassInterface
{
/**
* Passes MinkExtension's base_url parameter
*
* @param ContainerBuilder $container
*/
public function process(ContainerBuilder $container)
{
$frameworkPath = $container->getParameter('behat.silverstripe_extension.framework_path');
global $_FILE_TO_URL_MAPPING;
if ($container->getParameter('behat.mink.base_url')) {
// If base_url is already defined, also set it in the SilverStripe mapping
$_FILE_TO_URL_MAPPING[dirname($frameworkPath)] = $container->getParameter('behat.mink.base_url');
} elseif ($envPath = $this->findEnvironmentConfigFile($frameworkPath)) {
// Otherwise try to retrieve it from _ss_environment
include_once $envPath;
if (isset($_FILE_TO_URL_MAPPING)
&& !($container->hasParameter('behat.mink.base_url') && $container->getParameter('behat.mink.base_url'))
) {
$baseUrl = $this->findBaseUrlFromMapping(dirname($frameworkPath), $_FILE_TO_URL_MAPPING);
if ($baseUrl) {
$container->setParameter('behat.mink.base_url', $baseUrl);
}
}
}
if (!$container->getParameter('behat.mink.base_url')) {
throw new \InvalidArgumentException(
'"base_url" not configured. Please specify it in your behat.yml configuration, ' .
'or in your _ss_environment.php configuration through $_FILE_TO_URL_MAPPING'
);
}
// The Behat\MinkExtension\Extension class copies configuration into an internal hash,
// we need to follow this pattern to propagate our changes.
$parameters = $container->getParameter('behat.mink.parameters');
$parameters['base_url'] = $container->getParameter('behat.mink.base_url');
$container->setParameter('behat.mink.parameters', $parameters);
}
/**
* Try to auto-detect host for webroot based on _ss_environment.php data (unless explicitly set in behat.yml)
* Copied logic from Core.php, because it needs to be executed prior to {@link SilverStripeAwareInitializer}.
*
* @param string $path Absolute start path to search upwards from
* @return string Absolute path to environment file
*/
protected function findEnvironmentConfigFile($path)
{
$envPath = null;
$envFile = '_ss_environment.php'; //define the name of the environment file
$path = '.'; //define the dir to start scanning from (have to add the trailing slash)
//check this dir and every parent dir (until we hit the base of the drive)
do {
$path = realpath($path) . '/';
//if the file exists, then we include it, set relevant vars and break out
if (file_exists($path . $envFile)) {
$envPath = $path . $envFile;
break;
}
// here we need to check that the real path of the last dir and the next one are
// not the same, if they are, we have hit the root of the drive
} while (realpath($path) != realpath($path .= '../'));
return $envPath;
}
/**
* Copied logic from Core.php, because it needs to be executed prior to {@link SilverStripeAwareInitializer}.
*
* @param string $path Absolute start path to search upwards from
* @param array $mapping Map of paths to host names
* @return String URL
*/
protected function findBaseUrlFromMapping($path, $mapping)
{
$fullPath = $path;
$url = null;
while ($path && $path != "/" && !preg_match('/^[A-Z]:\\\\$/', $path)) {
if (isset($mapping[$path])) {
$url = $mapping[$path] . str_replace(DIRECTORY_SEPARATOR, '/', substr($fullPath, strlen($path)));
break;
} else {
$path = dirname($path); // traverse up
}
}
return $url;
}
}

View File

@ -1,248 +0,0 @@
<?php
namespace SilverStripe\BehatExtension\Console\Processor;
use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Behat\Behat\Console\Processor\InitProcessor as BaseProcessor;
use SilverStripe\Core\Manifest\ClassLoader;
/**
* Initializes a project for Behat usage, creating context files.
*/
class InitProcessor extends BaseProcessor
{
private $container;
/**
* @param ContainerInterface $container Container instance
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* @param Command $command
*/
public function configure(Command $command)
{
parent::configure($command);
$command->addOption(
'--namespace',
null,
InputOption::VALUE_OPTIONAL,
"Optional namespace for FeatureContext, defaults to <foldername>\\Test\\Behaviour.\n"
);
}
public function process(InputInterface $input, OutputInterface $output)
{
// throw exception if no features argument provided
if (!$input->getArgument('features') && $input->getOption('init')) {
throw new \InvalidArgumentException('Provide features argument in order to init suite.');
}
// initialize bundle structure and exit
if ($input->getOption('init')) {
$this->initBundleDirectoryStructure($input, $output);
exit(0);
}
}
/**
* Inits bundle directory structure
*
* @param InputInterface $input
* @param OutputInterface $output
*/
protected function initBundleDirectoryStructure(InputInterface $input, OutputInterface $output)
{
// Bootstrap SS so we can use module listing
$frameworkPath = $this->container->getParameter('behat.silverstripe_extension.framework_path');
$_GET['flush'] = 1;
require_once('Core/Core.php');
unset($_GET['flush']);
$featuresPath = $input->getArgument('features');
if (!$featuresPath) {
throw new \InvalidArgumentException('Please specify a module name (e.g. "@mymodule")');
}
// Can't use 'behat.paths.base' since that's locked at this point to base folder (not module)
$pathSuffix = $this->container->getParameter('behat.silverstripe_extension.context.path_suffix');
// get module from short notation if path starts from @
$currentModuleName = $this->container->getParameter('behat.silverstripe_extension.module');
if (preg_match('/^\@([^\/\\\\]+)(.*)$/', $featuresPath, $matches)) {
$currentModuleName = $matches[1];
}
if (!$currentModuleName) {
throw new \InvalidArgumentException('Can not find module to initialize suite.');
}
// Get path for module
$module = ModuleLoader::instance()->getManifest()->getModule($currentModuleName);
if (!$module) {
throw new \InvalidArgumentException(sprintf('Module "%s" not found', $currentModuleName));
}
$currentModulePath = $module->getPath();
// TODO Retrieve from module definition once that's implemented
if ($input->getOption('namespace')) {
$namespace = $input->getOption('namespace');
} else {
$namespace = ucfirst($currentModuleName);
}
$namespace .= '\\' . $this->container->getParameter('behat.silverstripe_extension.context.namespace_suffix');
$featuresPath = rtrim($currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix, DIRECTORY_SEPARATOR);
$basePath = $this->container->getParameter('behat.paths.base').DIRECTORY_SEPARATOR;
$bootstrapPath = $featuresPath.DIRECTORY_SEPARATOR.'bootstrap';
$contextPath = $bootstrapPath.DIRECTORY_SEPARATOR.'Context';
if (!is_dir($featuresPath)) {
mkdir($featuresPath, 0777, true);
mkdir($bootstrapPath, 0777, true);
// touch($bootstrapPath.DIRECTORY_SEPARATOR.'_manifest_exclude');
$output->writeln(
'<info>+d</info> ' .
str_replace($basePath, '', realpath($featuresPath)) .
' <comment>- place your *.feature files here</comment>'
);
}
if (!is_dir($contextPath)) {
mkdir($contextPath, 0777, true);
$className = $this->container->getParameter('behat.context.class');
file_put_contents(
$contextPath . DIRECTORY_SEPARATOR . $className . '.php',
strtr($this->getFeatureContextSkelet(), array(
'%NAMESPACE%' => $namespace
))
);
$output->writeln(
'<info>+f</info> ' .
str_replace($basePath, '', realpath($contextPath)) . DIRECTORY_SEPARATOR .
'FeatureContext.php <comment>- place your feature related code here</comment>'
);
}
}
/**
* {@inheritdoc}
*/
protected function getFeatureContextSkelet()
{
return <<<'PHP'
<?php
namespace %NAMESPACE%;
use SilverStripe\BehatExtension\Context\SilverStripeContext,
SilverStripe\BehatExtension\Context\BasicContext,
SilverStripe\BehatExtension\Context\LoginContext,
SilverStripe\BehatExtension\Context\FixtureContext,
SilverStripe\Framework\Test\Behaviour\CmsFormsContext,
SilverStripe\Framework\Test\Behaviour\CmsUiContext,
SilverStripe\Cms\Test\Behaviour;
/**
* Features context
*
* Context automatically loaded by Behat.
* Uses subcontexts to extend functionality.
*/
class FeatureContext extends SilverStripeContext {
/**
* @var FixtureFactory
*/
protected $fixtureFactory;
/**
* Initializes context.
* Every scenario gets it's own context object.
*
* @param array $parameters context parameters (set them up through behat.yml)
*/
public function __construct(array $parameters) {
parent::__construct($parameters);
$this->useContext('BasicContext', new BasicContext($parameters));
$this->useContext('LoginContext', new LoginContext($parameters));
$this->useContext('CmsFormsContext', new CmsFormsContext($parameters));
$this->useContext('CmsUiContext', new CmsUiContext($parameters));
$fixtureContext = new FixtureContext($parameters);
$fixtureContext->setFixtureFactory($this->getFixtureFactory());
$this->useContext('FixtureContext', $fixtureContext);
// Use blueprints to set user name from identifier
$factory = $fixtureContext->getFixtureFactory();
$blueprint = \Injector::inst()->create('FixtureBlueprint', 'Member');
$blueprint->addCallback('beforeCreate', function($identifier, &$data, &$fixtures) {
if(!isset($data['FirstName'])) $data['FirstName'] = $identifier;
});
$factory->define('Member', $blueprint);
// Auto-publish pages
if (class_exists('SiteTree')) {
foreach(\ClassInfo::subclassesFor('SiteTree') as $id => $class) {
$blueprint = \Injector::inst()->create('FixtureBlueprint', $class);
$blueprint->addCallback('afterCreate', function($obj, $identifier, &$data, &$fixtures) {
$obj->publish('Stage', 'Live');
});
$factory->define($class, $blueprint);
}
}
}
public function setMinkParameters(array $parameters) {
parent::setMinkParameters($parameters);
if(isset($parameters['files_path'])) {
$this->getSubcontext('FixtureContext')->setFilesPath($parameters['files_path']);
}
}
/**
* @return FixtureFactory
*/
public function getFixtureFactory() {
if(!$this->fixtureFactory) {
$this->fixtureFactory = \Injector::inst()->create('BehatFixtureFactory');
}
return $this->fixtureFactory;
}
public function setFixtureFactory(FixtureFactory $factory) {
$this->fixtureFactory = $factory;
}
//
// Place your definition and hook methods here:
//
// /**
// * @Given /^I have done something with "([^"]*)"$/
// */
// public function iHaveDoneSomethingWith($argument) {
// $container = $this->kernel->getContainer();
// $container->get('some_service')->doSomethingWith($argument);
// }
//
}
PHP;
}
}

View File

@ -1,125 +0,0 @@
<?php
namespace SilverStripe\BehatExtension\Console\Processor;
use SilverStripe\Core\Manifest\ModuleLoader;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Behat\Behat\Console\Processor\LocatorProcessor as BaseProcessor;
/**
* Path locator processor.
*/
class LocatorProcessor extends BaseProcessor
{
private $container;
/**
* Constructs processor.
*
* @param ContainerInterface $container Container instance
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Configures command to be able to process it later.
*
* @param Command $command
*/
public function configure(Command $command)
{
$command->addArgument(
'features',
InputArgument::OPTIONAL,
"Feature(s) to run. Could be:".
"\n- a dir (<comment>src/to/module/Features/</comment>), " .
"\n- a feature (<comment>src/to/module/Features/*.feature</comment>), " .
"\n- a scenario at specific line (<comment>src/to/module/Features/*.feature:10</comment>). " .
"\n- Also, you can use short module notation (<comment>@moduleName/*.feature:10</comment>)"
);
}
/**
* Processes data from container and console input.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @throws \RuntimeException
*/
public function process(InputInterface $input, OutputInterface $output)
{
$featuresPath = $input->getArgument('features');
// Can't use 'behat.paths.base' since that's locked at this point to base folder (not module)
$pathSuffix = $this->container->getParameter('behat.silverstripe_extension.context.path_suffix');
$currentModuleName = null;
// get module specified in behat.yml
$currentModuleName = $this->container->getParameter('behat.silverstripe_extension.module');
// get module from short notation if path starts from @
if ($featuresPath && preg_match('/^\@([^\/\\\\]+)(.*)$/', $featuresPath, $matches)) {
$currentModuleName = $matches[1];
// TODO Replace with proper module loader once AJShort's changes are merged into core
$module = ModuleLoader::instance()->getManifest()->getModule($currentModuleName);
if (!$module) {
throw new \InvalidArgumentException(sprintf('Module "%s" not found', $currentModuleName));
}
$currentModulePath = $module->getPath();
$featuresPath = str_replace(
'@'.$currentModuleName,
$currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix,
$featuresPath
);
// get module from provided features path
} elseif (!$currentModuleName && $featuresPath) {
$path = realpath(preg_replace('/\.feature\:.*$/', '.feature', $featuresPath));
$modules = ModuleLoader::instance()->getManifest()->getModules();
$currentModulePath = null;
foreach ($modules as $module) {
$modulePath = $module->getPath();
if (false !== strpos($path, realpath($modulePath))) {
$currentModuleName = $module->getName();
$currentModulePath = realpath($modulePath);
break;
}
}
if (!$currentModulePath) {
throw new \InvalidArgumentException(sprintf('Module not found in path "%s"', $featuresPath));
}
$featuresPath = $currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix.DIRECTORY_SEPARATOR.$featuresPath;
// if module is configured for profile and feature provided
} elseif ($currentModuleName && $featuresPath) {
$module = ModuleLoader::instance()->getManifest()->getModule($currentModuleName);
if (!$module) {
throw new \InvalidArgumentException(sprintf('Module "%s" not found', $currentModuleName));
}
$currentModulePath = $module->getPath();
$featuresPath = $currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix.DIRECTORY_SEPARATOR.$featuresPath;
}
if ($input->getOption('namespace')) {
$namespace = $input->getOption('namespace');
} else {
$namespace = ucfirst($currentModuleName);
}
if ($currentModuleName) {
$this->container
->get('behat.silverstripe_extension.context.class_guesser')
// TODO Improve once modules can declare their own namespaces consistently
->setNamespaceBase($namespace);
}
$this->container
->get('behat.console.command')
->setFeaturesPaths($featuresPath ? array($featuresPath) : array());
}
}

View File

@ -1,58 +0,0 @@
<?php
namespace SilverStripe\BehatExtension\Context\ClassGuesser;
use Behat\Behat\Context\ClassGuesser\ClassGuesserInterface;
/**
* Module context class guesser.
* Provides module context class if found.
*/
class ModuleContextClassGuesser implements ClassGuesserInterface
{
private $namespaceSuffix;
private $namespaceBase;
private $contextClass;
/**
* Initializes guesser.
*
* @param string $namespaceSuffix
* @param string $contextClass
*/
public function __construct($namespaceSuffix, $contextClass)
{
$this->namespaceSuffix = $namespaceSuffix;
$this->contextClass = $contextClass;
}
/**
* Sets bundle namespace to use for guessing.
*
* @param string $namespaceBase
* @return $this
*/
public function setNamespaceBase($namespaceBase)
{
$this->namespaceBase = $namespaceBase;
return $this;
}
/**
* Tries to guess context classname.
*
* @return string
*/
public function guess()
{
// Try fully qualified namespace
if (class_exists($class = $this->namespaceBase.'\\'.$this->namespaceSuffix.'\\'.$this->contextClass)) {
return $class;
}
// Fall back to namespace with SilverStripe prefix
// TODO Remove once core has namespace capabilities for modules
if (class_exists($class = 'SilverStripe\\'.$this->namespaceBase.'\\'.$this->namespaceSuffix.'\\'.$this->contextClass)) {
return $class;
}
}
}

View File

@ -1,187 +0,0 @@
<?php
namespace SilverStripe\BehatExtension\Context;
use Behat\Behat\Context\BehatContext;
use Behat\Behat\Context\Step;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Group;
use SilverStripe\Security\Member;
// PHPUnit
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
/**
* LoginContext
*
* Context used to define steps related to login and logout functionality
*/
class LoginContext extends BehatContext
{
protected $context;
/**
* Cache for logInWithPermission()
*/
protected $cache_generatedMembers = array();
/**
* Initializes context.
* Every scenario gets it's own context object.
*
* @param array $parameters context parameters (set them up through behat.yml)
*/
public function __construct(array $parameters)
{
// Initialize your context here
$this->context = $parameters;
}
/**
* Get Mink session from MinkContext
*/
public function getSession($name = null)
{
return $this->getMainContext()->getSession($name);
}
/**
* @Given /^I am logged in$/
*/
public function stepIAmLoggedIn()
{
$c = $this->getMainContext();
$adminUrl = $c->joinUrlParts($c->getBaseUrl(), $c->getAdminUrl());
$loginUrl = $c->joinUrlParts($c->getBaseUrl(), $c->getLoginUrl());
$this->getSession()->visit($adminUrl);
if (0 == strpos($this->getSession()->getCurrentUrl(), $loginUrl)) {
$this->stepILogInWith('admin', 'password');
assertStringStartsWith($adminUrl, $this->getSession()->getCurrentUrl());
}
}
/**
* Creates a member in a group with the correct permissions.
* Example: Given I am logged in with "ADMIN" permissions
*
* @Given /^I am logged in with "([^"]*)" permissions$/
*/
public function iAmLoggedInWithPermissions($permCode)
{
if (!isset($this->cache_generatedMembers[$permCode])) {
$group = Group::get()->filter('Title', "$permCode group")->first();
if (!$group) {
$group = Injector::inst()->create('SilverStripe\\Security\\Group');
}
$group->Title = "$permCode group";
$group->write();
$permission = Injector::inst()->create('SilverStripe\\Security\\Permission');
$permission->Code = $permCode;
$permission->write();
$group->Permissions()->add($permission);
$member = DataObject::get_one('SilverStripe\\Security\\Member', sprintf('"Email" = \'%s\'', "$permCode@example.org"));
if (!$member) {
$member = Injector::inst()->create('SilverStripe\\Security\\Member');
}
// make sure any validation for password is skipped, since we're not testing complexity here
$validator = Member::password_validator();
Member::set_password_validator(null);
$member->FirstName = $permCode;
$member->Surname = "User";
$member->Email = "$permCode@example.org";
$member->PasswordEncryption = "none";
$member->changePassword('Secret!123');
$member->write();
$group->Members()->add($member);
Member::set_password_validator($validator);
$this->cache_generatedMembers[$permCode] = $member;
}
return new Step\Given(sprintf('I log in with "%s" and "%s"', "$permCode@example.org", 'Secret!123'));
}
/**
* @Given /^I am not logged in$/
*/
public function stepIAmNotLoggedIn()
{
$c = $this->getMainContext();
$this->getSession()->visit($c->joinUrlParts($c->getBaseUrl(), 'Security/logout'));
}
/**
* @When /^I log in with "(?<username>[^"]*)" and "(?<password>[^"]*)"$/
*/
public function stepILogInWith($email, $password)
{
$c = $this->getMainContext();
$loginUrl = $c->joinUrlParts($c->getBaseUrl(), $c->getLoginUrl());
$this->getSession()->visit($loginUrl);
$page = $this->getSession()->getPage();
$forms = $page->findAll('xpath', '//form[contains(@action, "Security/LoginForm")]');
assertNotNull($forms, 'Login form not found');
// Try to find visible forms again on login page.
$visibleForm = null;
foreach ($forms as $form) {
if ($form->isVisible() && $form->find('css', '[name=Email]')) {
$visibleForm = $form;
}
}
assertNotNull($visibleForm, 'Could not find login form');
$emailField = $visibleForm->find('css', '[name=Email]');
$passwordField = $visibleForm->find('css', '[name=Password]');
$submitButton = $visibleForm->find('css', '[type=submit]');
$securityID = $visibleForm->find('css', '[name=SecurityID]');
assertNotNull($emailField, 'Email field on login form not found');
assertNotNull($passwordField, 'Password field on login form not found');
assertNotNull($submitButton, 'Submit button on login form not found');
// @todo Once CSRF is mandatory, uncomment this
// assertNotNull($securityID, 'CSRF token not found');
$emailField->setValue($email);
$passwordField->setValue($password);
$submitButton->press();
}
/**
* @Given /^I should see a log-in form$/
*/
public function stepIShouldSeeALogInForm()
{
$page = $this->getSession()->getPage();
$loginForm = $page->find('css', '#MemberLoginForm_LoginForm');
assertNotNull($loginForm, 'I should see a log-in form');
}
/**
* @Then /^I will see a "([^"]*)" log-in message$/
*/
public function stepIWillSeeALogInMessage($type)
{
$page = $this->getSession()->getPage();
$message = $page->find('css', sprintf('.message.%s', $type));
assertNotNull($message, sprintf('%s message not found.', $type));
}
/**
* @Then /^the password for "([^"]*)" should be "([^"]*)"$/
*/
public function stepPasswordForEmailShouldBe($id, $password)
{
$member = Member::get()->filter('SilverStripe\\Control\\Email\\Email', $id)->First();
assertNotNull($member);
assertTrue($member->checkPassword($password)->valid());
}
}

View File

@ -1,34 +0,0 @@
<?php
namespace SilverStripe\BehatExtension\Context;
/*
* This file is part of the Behat/SilverStripeExtension
*
* (c) Michał Ochman <ochman.d.michal@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* SilverStripe aware interface for contexts.
*
* @author Michał Ochman <ochman.d.michal@gmail.com>
*/
interface SilverStripeAwareContextInterface
{
/**
* Sets SilverStripe instance.
*
* @param string $databaseName Temp database name
*/
public function setDatabase($databaseName);
/**
* Marks steps as AJAX steps for special treatment
*
* @param array $ajaxSteps Array of step name parts to match
*/
public function setAjaxSteps($ajaxSteps);
}

View File

@ -1,122 +0,0 @@
<?php
namespace SilverStripe\BehatExtension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Behat\Behat\Extension\ExtensionInterface;
/*
* This file is part of the SilverStripe\BehatExtension
*
* (c) Michał Ochman <ochman.d.michal@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* SilverStripe extension for Behat class.
*
* @author Michał Ochman <ochman.d.michal@gmail.com>
*/
class Extension implements ExtensionInterface
{
/**
* Loads a specific configuration.
*
* @param array $config Extension configuration hash (from behat.yml)
* @param ContainerBuilder $container ContainerBuilder instance
*/
public function load(array $config, ContainerBuilder $container)
{
if (!isset($config['framework_path'])) {
throw new \InvalidArgumentException('Specify `framework_path` parameter for silverstripe_extension');
}
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/services'));
$loader->load('silverstripe.yml');
$behatBasePath = $container->getParameter('behat.paths.base');
$config['framework_path'] = realpath(sprintf(
'%s%s%s',
rtrim($behatBasePath, DIRECTORY_SEPARATOR),
DIRECTORY_SEPARATOR,
ltrim($config['framework_path'], DIRECTORY_SEPARATOR)
));
if (!file_exists($config['framework_path']) || !is_dir($config['framework_path'])) {
throw new \InvalidArgumentException('Path specified as `framework_path` either doesn\'t exist or is not a directory');
}
$container->setParameter('behat.silverstripe_extension.framework_path', $config['framework_path']);
$container->setParameter('behat.silverstripe_extension.admin_url', $config['admin_url']);
$container->setParameter('behat.silverstripe_extension.login_url', $config['login_url']);
$container->setParameter('behat.silverstripe_extension.screenshot_path', $config['screenshot_path']);
$container->setParameter('behat.silverstripe_extension.ajax_timeout', $config['ajax_timeout']);
if (isset($config['ajax_steps'])) {
$container->setParameter('behat.silverstripe_extension.ajax_steps', $config['ajax_steps']);
}
if (isset($config['region_map'])) {
$container->setParameter('behat.silverstripe_extension.region_map', $config['region_map']);
}
$container->setParameter('behat.silverstripe_extension.bootstrap_file', $config['bootstrap_file']);
}
/**
* @return array
*/
public function getCompilerPasses()
{
return array(
new Compiler\CoreInitializationPass()
);
}
/**
* Setups configuration for current extension.
*
* @param ArrayNodeDefinition $builder
*/
public function getConfig(ArrayNodeDefinition $builder)
{
$builder->
children()->
scalarNode('framework_path')->
defaultValue('framework')->
end()->
scalarNode('screenshot_path')->
defaultNull()->
end()->
arrayNode('region_map')->
useAttributeAsKey('key')->
prototype('variable')->end()->
end()->
scalarNode('admin_url')->
defaultValue('/admin/')->
end()->
scalarNode('login_url')->
defaultValue('/Security/login')->
end()->
scalarNode('ajax_timeout')->
defaultValue(5000)->
end()->
scalarNode('bootstrap_file')->
defaultNull()->
end()->
arrayNode('ajax_steps')->
defaultValue(array(
'go to',
'follow',
'press',
'click',
'submit'
))->
prototype('scalar')->
end()->
end()->
end();
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace SilverStripe\BehatExtension;
/**
* Subclass the main extension in order to get a say in the config compilation.
* We need to intercept setting the base_url to auto-detect it from SilverStripe configuration.
*/
class MinkExtension extends \Behat\MinkExtension\Extension
{
public function getCompilerPasses()
{
return array_merge(
array(new Compiler\MinkExtensionBaseUrlPass()),
parent::getCompilerPasses()
);
}
}

View File

@ -1,36 +0,0 @@
parameters:
behat.silverstripe_extension.context.initializer.class: SilverStripe\BehatExtension\Context\Initializer\SilverStripeAwareInitializer
behat.silverstripe_extension.context.class_guesser.class: SilverStripe\BehatExtension\Context\ClassGuesser\ModuleContextClassGuesser
behat.console.processor.locator.class: SilverStripe\BehatExtension\Console\Processor\LocatorProcessor
behat.console.processor.init.class: SilverStripe\BehatExtension\Console\Processor\InitProcessor
behat.silverstripe_extension.context.namespace_suffix: Test\Behaviour
behat.silverstripe_extension.framework_path: framework
behat.silverstripe_extension.ajax_steps: ~
behat.silverstripe_extension.ajax_timeout: ~
behat.silverstripe_extension.admin_url: ~
behat.silverstripe_extension.login_url: ~
behat.silverstripe_extension.screenshot_path: ~
behat.silverstripe_extension.module:
behat.silverstripe_extension.region_map: ~
behat.silverstripe_extension.context.path_suffix: tests/behat/features/
services:
behat.silverstripe_extension.context.initializer:
class: %behat.silverstripe_extension.context.initializer.class%
arguments:
- %behat.silverstripe_extension.framework_path%
calls:
- [setAjaxSteps, [%behat.silverstripe_extension.ajax_steps%]]
- [setAjaxTimeout, [%behat.silverstripe_extension.ajax_timeout%]]
- [setAdminUrl, [%behat.silverstripe_extension.admin_url%]]
- [setLoginUrl, [%behat.silverstripe_extension.login_url%]]
- [setScreenshotPath, [%behat.silverstripe_extension.screenshot_path%]]
- [setRegionMap, [%behat.silverstripe_extension.region_map%]]
tags:
- { name: behat.context.initializer }
behat.silverstripe_extension.context.class_guesser:
class: %behat.silverstripe_extension.context.class_guesser.class%
arguments:
- %behat.silverstripe_extension.context.namespace_suffix%
- %behat.context.class%
tags:
- { name: behat.context.class_guesser, priority: 10 }

View File

@ -3,7 +3,6 @@
namespace SilverStripe\BehatExtension\Utility;
use SilverStripe\Dev\TestMailer as BaseTestMailer;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\TestSession\TestSessionEnvironment;
/**
@ -13,7 +12,6 @@ use SilverStripe\TestSession\TestSessionEnvironment;
*/
class TestMailer extends BaseTestMailer
{
/**
* @var TestSessionEnvironment
*/
@ -36,17 +34,6 @@ class TestMailer extends BaseTestMailer
$this->testSessionEnvironment->applyState($state);
}
/**
* Search for an email that was sent.
* All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
*
* @param $to
* @param $from
* @param $subject
* @param $content
* @return array Contains the keys: 'type', 'to', 'from', 'subject', 'content', 'plainContent', 'attachedFiles',
* 'customHeaders', 'htmlContent', 'inlineImages'
*/
public function findEmail($to = null, $from = null, $subject = null, $content = null)
{
$matches = $this->findEmails($to, $from, $subject, $content);

View File

@ -0,0 +1,22 @@
<?php
namespace $Namespace;
use SilverStripe\\BehatExtension\\Context\\SilverStripeContext;
/**
* Default context for this module
*/
class FeatureContext extends SilverStripeContext
{
//
// Place your definition and hook methods here:
//
// /**
// * @Given /^I have done something with "([^"]*)"$/
// */
// public function iHaveDoneSomethingWith($argument) {
// assert($argument);
// }
//
}

View File

@ -0,0 +1,14 @@
Feature: Addition
As a user
I want to do a thing
So that I can have a result
Background:
Given a base condition I want for all scenarios
Scenario: Add two numbers
Given a condition
And another condition
When I do an action
And I do another action
Then I should see the expected result

10
templates/config-base.yml Normal file
View File

@ -0,0 +1,10 @@
default:
suites: []
extensions:
SilverStripe\BehatExtension\MinkExtension:
default_session: selenium2
javascript_session: selenium2
selenium2:
browser: firefox
SilverStripe\BehatExtension\Extension:
screenshot_path: %paths.base%/artifacts/screenshots

View File

@ -1,8 +1,14 @@
<?php
namespace SilverStripe\BehatExtension\Tests;
use SilverStripe\BehatExtension\Context\SilverStripeContext;
use Behat\Mink\Element\DocumentElement;
use Behat\Mink\Selector\SelectorsHandler;
use Behat\Mink\Session;
use Behat\Mink\Mink;
use Behat\Mink\Driver\DriverInterface;
use Behat\Mink\Element\Element;
use SilverStripe\BehatExtension\Tests\SilverStripeContextTest\FeatureContext;
class SilverStripeContextTest extends \PHPUnit_Framework_TestCase
{
@ -56,16 +62,19 @@ class SilverStripeContextTest extends \PHPUnit_Framework_TestCase
$this->assertNotNull($obj);
}
/**
* @return FeatureContext
*/
protected function getContextMock()
{
$pageMock = $this->getMockBuilder('Behat\Mink\Element\DocumentElement')
$pageMock = $this->getMockBuilder(DocumentElement::class)
->disableOriginalConstructor()
->setMethods(array('find'))
->getMock();
$sessionMock = $this->getMockBuilder('Behat\Mink\Session')
$sessionMock = $this->getMockBuilder(Session::class)
->setConstructorArgs(array(
$this->getMockBuilder('Behat\Mink\Driver\DriverInterface')->getMock(),
$this->getMockBuilder('Behat\Mink\Selector\SelectorsHandler')->getMock()
$this->getMockBuilder(DriverInterface::class)->getMock(),
$this->getMockBuilder(SelectorsHandler::class)->getMock()
))
->setMethods(array('getPage'))
->getMock();
@ -75,15 +84,18 @@ class SilverStripeContextTest extends \PHPUnit_Framework_TestCase
$mink = new Mink(array('default' => $sessionMock));
$mink->setDefaultSessionName('default');
$context = new SilverStripeContext(array());
$context = new FeatureContext(array());
$context->setMink($mink);
return $context;
}
/**
* @return Element|\PHPUnit_Framework_MockObject_MockObject
*/
protected function getElementMock()
{
return $this->getMockBuilder('Behat\Mink\Element\Element')
return $this->getMockBuilder(Element::class)
->disableOriginalConstructor()
->getMock();
}

View File

@ -0,0 +1,11 @@
<?php
namespace SilverStripe\BehatExtension\Tests\SilverStripeContextTest;
use SilverStripe\BehatExtension\Context\SilverStripeContext;
use SilverStripe\Dev\TestOnly;
class FeatureContext extends SilverStripeContext implements TestOnly
{
}