mirror of
https://github.com/silverstripe/silverstripe-behat-extension
synced 2024-10-22 17:05:32 +02:00
API Upgrade to behat 3
This commit is contained in:
parent
78c65719da
commit
9230ce2405
@ -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
|
||||
|
@ -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
|
||||
|
70
README.md
70
README.md
@ -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
|
||||
|
@ -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
29
config/silverstripe.yml
Normal 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 }
|
@ -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
|
40
src/Compiler/MinkExtensionBaseUrlPass.php
Normal file
40
src/Compiler/MinkExtensionBaseUrlPass.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
));
|
@ -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);
|
181
src/Context/LoginContext.php
Normal file
181
src/Context/LoginContext.php
Normal 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;
|
||||
}
|
||||
}
|
63
src/Context/MainContextAwareTrait.php
Normal file
63
src/Context/MainContextAwareTrait.php
Normal 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");
|
||||
}
|
||||
}
|
25
src/Context/RetryableContextTrait.php
Normal file
25
src/Context/RetryableContextTrait.php
Normal 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;
|
||||
}
|
||||
}
|
71
src/Context/SilverStripeAwareContext.php
Normal file
71
src/Context/SilverStripeAwareContext.php
Normal 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);
|
||||
}
|
@ -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);
|
28
src/Controllers/ModuleCommandTrait.php
Normal file
28
src/Controllers/ModuleCommandTrait.php
Normal 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;
|
||||
}
|
||||
}
|
288
src/Controllers/ModuleInitialisationController.php
Normal file
288
src/Controllers/ModuleInitialisationController.php
Normal 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>"
|
||||
);
|
||||
}
|
||||
}
|
171
src/Controllers/ModuleSuiteLocator.php
Normal file
171
src/Controllers/ModuleSuiteLocator.php
Normal 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
164
src/Extension.php
Normal 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
23
src/MinkExtension.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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 }
|
@ -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);
|
22
templates/FeatureContext.ss
Normal file
22
templates/FeatureContext.ss
Normal 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);
|
||||
// }
|
||||
//
|
||||
}
|
14
templates/SkeletonFeature.ss
Normal file
14
templates/SkeletonFeature.ss
Normal 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
10
templates/config-base.yml
Normal 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
|
@ -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();
|
||||
}
|
11
tests/php/SilverStripeContextTest/FeatureContext.php
Normal file
11
tests/php/SilverStripeContextTest/FeatureContext.php
Normal 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
|
||||
{
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user