API Upgrade to behat 3

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

View File

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

View File

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

148
README.md
View File

@ -5,9 +5,9 @@
## Overview ## Overview
[Behat](http://behat.org) is a testing framework for behaviour-driven development. [Behat](http://behat.org) is a testing framework for behaviour-driven development.
Because it primarily interacts with your website through a browser, Because it primarily interacts with your website through a browser,
you don't need any specific integration tools to get it going with you don't need any specific integration tools to get it going with
a basic SilverStripe website, simply follow the a basic SilverStripe website, simply follow the
[standard Behat usage instructions](http://docs.behat.org/). [standard Behat usage instructions](http://docs.behat.org/).
This extension comes in handy if you want to go beyond This extension comes in handy if you want to go beyond
@ -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). Simply [install SilverStripe through Composer](http://doc.silverstripe.org/framework/en/installation/composer).
Skip this step if adding the module to an existing project. 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. Switch to the newly created webroot, and add the SilverStripe Behat extension.
@ -56,13 +56,13 @@ It might be older than your currently installed Firefox.
It's important to have a browser that's [supported by Selenium-Webdriver](http://docs.seleniumhq.org/docs/01_introducing_selenium.jsp#selenium-webdriver) It's important to have a browser that's [supported by Selenium-Webdriver](http://docs.seleniumhq.org/docs/01_introducing_selenium.jsp#selenium-webdriver)
Now install the SilverStripe project as usual by opening it in a browser and following the instructions. Now install the SilverStripe project as usual by opening it in a browser and following the instructions.
Protip: You can skip this step by using `[SS_DATABASE_CHOOSE_NAME]` in a global Protip: You can skip this step by using `[SS_DATABASE_CHOOSE_NAME]` in a global
[`_ss_environment.php`](http://doc.silverstripe.org/framework/en/topics/environment-management) [`_ss_environment.php`](http://doc.silverstripe.org/framework/en/topics/environment-management)
file one level above the webroot. file one level above the webroot.
Unless you have [`$_FILE_TO_URL_MAPPING`](http://doc.silverstripe.org/framework/en/topics/commandline#configuration) Unless you have [`$_FILE_TO_URL_MAPPING`](http://doc.silverstripe.org/framework/en/topics/commandline#configuration)
set up, you also need to specify the URL for your webroot. Either add it to the existing `behat.yml` configuration file set up, you also need to specify the URL for your webroot. Either add it to the existing `behat.yml` configuration file
in your project root, or set is as an environment variable in your terminal session: in your project root, or set is as an environment variable in your terminal session:
export BEHAT_PARAMS="extensions[SilverStripe\BehatExtension\MinkExtension][base_url]=http://localhost/" export BEHAT_PARAMS="extensions[SilverStripe\BehatExtension\MinkExtension][base_url]=http://localhost/"
@ -94,10 +94,6 @@ Now you can run the tests (for example for the `framework` module):
vendor/bin/behat @framework 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): Or even run a single scenario by it's name (supports regular expressions):
vendor/bin/behat --name 'My scenario title' @framework vendor/bin/behat --name 'My scenario title' @framework
@ -118,77 +114,77 @@ The SilverStripe installer already comes with a YML configuration
which is ready to run tests on a locally hosted Selenium server, which is ready to run tests on a locally hosted Selenium server,
located in the project root as `behat.yml`. located in the project root as `behat.yml`.
You'll need to customize at least the `base_url` setting to match the URL where You should ensure that you have configured SS_BASE_URL in your `.env`.
the tested SilverStripe instance is hosted locally. This
Generic Mink configuration settings are placed in `SilverStripe\BehatExtension\MinkExtension`, Generic Mink configuration settings are placed in `SilverStripe\BehatExtension\MinkExtension`,
which is a subclass of `Behat\MinkExtension\Extension`. which is a subclass of `Behat\MinkExtension\Extension`.
Overview of settings (all in the `extensions.SilverStripe\BehatExtension\Extension` path): 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 * `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 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 Optional `ajax_steps` is used to match steps defined there so they can be "caught" by
[special AJAX handlers](http://blog.scur.pl/2012/06/ajax-callback-support-behat-mink/) that tweak the delays. You can either use a pipe delimited string or a list of substrings that match step definition. [special AJAX handlers](http://blog.scur.pl/2012/06/ajax-callback-support-behat-mink/) that tweak the delays. You can either use a pipe delimited string or a list of substrings that match step definition.
* `ajax_timeout`: Milliseconds after which an Ajax request is regarded as timed out, * `ajax_timeout`: Milliseconds after which an Ajax request is regarded as timed out,
and the script continues with its assertions to avoid a deadlock (Default: 5000). and the script continues with its assertions to avoid a deadlock (Default: 5000).
* `screenshot_path`: Absolute path used to store screenshot of a last known state * `screenshot_path`: Absolute path used to store screenshot of a last known state
of a failed step. of a failed step.
Screenshot names within that directory consist of feature file filename and line Screenshot names within that directory consist of feature file filename and line
number that failed. number that failed.
Example: behat.yml Example: behat.yml
default: default:
context: suites:
class: SilverStripe\MyModule\Test\Behaviour\FeatureContext 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: extensions:
SilverStripe\BehatExtension\Extension: SilverStripe\BehatExtension\MinkExtension:
screenshot_path: %behat.paths.base%/artifacts/screenshots default_session: selenium2
SilverStripe\BehatExtension\MinkExtension: javascript_session: selenium2
# Adjust this to your local environment selenium2:
base_url: http://localhost/ browser: firefox
default_session: selenium2 SilverStripe\BehatExtension\Extension:
javascript_session: selenium2 screenshot_path: %paths.base%/artifacts/screenshots
selenium2:
browser: firefox
## Module Initialization ## Module Initialization
You're all set to start writing features now! Simply create `*.feature` files You're all set to start writing features now! Simply create `*.feature` files
anywhere in your codebase, and run them as shown above. We recommend the folder anywhere in your codebase, and run them as shown above. We recommend the folder
structure of `tests/behat/features`, since its consistent with the common location structure of `tests/behat/features`, since its consistent with the common location
of SilverStripe's PHPUnit tests. of SilverStripe's PHPUnit tests.
Behat tests rely on a `FeatureContext` class which contains step definitions, Behat tests rely on a `FeatureContext` class which contains step definitions,
and can be composed of other subcontexts, e.g. for SilverStripe-specific CMS steps and can be composed of other subcontexts, e.g. for SilverStripe-specific CMS steps
(details on [behat.org](http://docs.behat.org/quick_intro.html#the-context-class-featurecontext)). (details on [behat.org](http://docs.behat.org/quick_intro.html#the-context-class-featurecontext)).
Since step definitions are quite domain specific, its likely that you'll need your own context. Since step definitions are quite domain specific, its likely that you'll need your own context.
The SilverStripe Behat extension provides an initializer script which generates a template The SilverStripe Behat extension provides an initializer script which generates a template
in the recommended folder structure: 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`, Note: namespace is mandatory
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:
vendor/bin/behat --namespace='MyVendor\MyModule' --init @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.
In this case, you'll need to pass in the namespace when running the features as well Also a folder for your features with `mymodule/tests/behat/features` will be created.
(at least until SilverStripe modules allow declaring a namespace). A `mymodule/behat.yml` is built, with a default suite named after the module.
vendor/bin/behat --namespace='MyVendor\MyModule' @mymodule
## Available Step Definitions ## Available Step Definitions
The extension comes with several `BehatContext` subclasses come with some extra step defintions. The extension comes with several `BehatContext` subclasses come with some extra step defintions.
Some of them are just helpful in general website testing, other's are specific to SilverStripe. Some of them are just helpful in general website testing, other's are specific to SilverStripe.
To find out all available steps (and the files they are defined in), run the following: To find out all available steps (and the files they are defined in), run the following:
vendor/bin/behat @mymodule --definitions=i vendor/bin/behat @mymodule --definitions=i
@ -200,7 +196,7 @@ In addition to the dynamic list, a cheatsheet of available steps can be found at
## Fixtures ## Fixtures
Since each test run creates a new database, you can't rely on existing state unless Since each test run creates a new database, you can't rely on existing state unless
you explicitly define it. you explicitly define it.
### Database Defaults ### Database Defaults
@ -222,22 +218,22 @@ use the inline definition syntax. The following example shows some syntax variat
Background: Background:
# Creates a new page without data. Can be accessed later under this identifier # Creates a new page without data. Can be accessed later under this identifier
Given a "page" "Page 1" Given a "page" "Page 1"
# Uses a custom RegistrationPage type # Uses a custom RegistrationPage type
And an "error page" "Register" And an "error page" "Register"
# Creates a page with inline properties # Creates a page with inline properties
And a "page" "Page 2" with "URLSegment"="page-1" and "Content"="my page 1" And a "page" "Page 2" with "URLSegment"="page-1" and "Content"="my page 1"
# Field names can be tabular, and based on DataObject::$field_labels # Field names can be tabular, and based on DataObject::$field_labels
And the "page" "Page 3" has the following data And the "page" "Page 3" has the following data
| Content | <blink> | | Content | <blink> |
| My Property | foo | | My Property | foo |
| My Boolean | bar | | My Boolean | bar |
# Pages are published by default, can be explicitly unpublished # Pages are published by default, can be explicitly unpublished
And the "page" "Page 1" is not published And the "page" "Page 1" is not published
# Create a hierarchy, and reference a record created earlier # Create a hierarchy, and reference a record created earlier
And the "page" "Page 1.1" is a child of a "page" "Page 1" And the "page" "Page 1.1" is a child of a "page" "Page 1"
# Specific page type step # Specific page type step
And a "page" "My Redirect" which redirects to a "page" "Page 1" And a "page" "My Redirect" which redirects to a "page" "Page 1"
And a "member" "Website User" with "FavouritePage"="=>Page.Page 1" And a "member" "Website User" with "FavouritePage"="=>Page.Page 1"
@javascript @javascript
@ -247,9 +243,9 @@ use the inline definition syntax. The following example shows some syntax variat
Then I should see "Page 1" in CMS Tree Then I should see "Page 1" in CMS Tree
* Fixtures are created where you defined them. If you want the fixtures to be created * Fixtures are created where you defined them. If you want the fixtures to be created
before every scenario, define them in [Background](http://docs.behat.org/guides/1.gherkin.html#backgrounds). before every scenario, define them in [Background](http://docs.behat.org/guides/1.gherkin.html#backgrounds).
If you want them to be created only when a particular scenario runs, define them there. If you want them to be created only when a particular scenario runs, define them there.
* Fixtures are cleared between scenarios. * Fixtures are cleared between scenarios.
* The basic syntax works for all `DataObject` subclasses, but some specific * The basic syntax works for all `DataObject` subclasses, but some specific
notations like "is not published" requires extensions like `Hierarchy` to be applied to the class notations like "is not published" requires extensions like `Hierarchy` to be applied to the class
* Record types, identifiers, property names and property values need to be quoted * Record types, identifiers, property names and property values need to be quoted
@ -265,9 +261,10 @@ use the inline definition syntax. The following example shows some syntax variat
### Directory Structure ### Directory Structure
As a convention, SilverStripe Behat tests live in a `tests/behat` subfolder 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 ### FeatureContext
@ -276,29 +273,20 @@ here as well. The only major difference is the base class from which
to extend your own `FeatureContext`: It should be `SilverStripeContext` to extend your own `FeatureContext`: It should be `SilverStripeContext`
rather than `BehatContext`. rather than `BehatContext`.
Example: mymodule/tests/behat/features/bootstrap/MyModule/Test/Behaviour/FeatureContext.php Example: mymodule/tests/behat/src/FeatureContext.php
<?php <?php
namespace MyModule\Test\Behaviour; namespace MyModule\Test\Behaviour;
use SilverStripe\BehatExtension\Context\SilverStripeContext, use SilverStripe\BehatExtension\Context\SilverStripeContext;
SilverStripe\BehatExtension\Context\BasicContext,
SilverStripe\BehatExtension\Context\LoginContext;
class FeatureContext extends 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 ### Screen Size
In some Selenium drivers you can In some Selenium drivers you can
define the desired browser window size through a `capabilities` definition. define the desired browser window size through a `capabilities` definition.
By default, Selenium doesn't support this though, so we've added a workaround By default, Selenium doesn't support this though, so we've added a workaround
through an environment variable: through an environment variable:
@ -308,7 +296,7 @@ through an environment variable:
### Inspecting PHP sessions ### Inspecting PHP sessions
Behat is executed from CLI, which in turn triggers web requests in a browser. Behat is executed from CLI, which in turn triggers web requests in a browser.
This browser session is associated PHP session information such as the logged-in user. This browser session is associated PHP session information such as the logged-in user.
After every request, the session information is persisted on disk as part After every request, the session information is persisted on disk as part
of the `TestSessionEnvironment`, in order to share it with Behat CLI. of the `TestSessionEnvironment`, in order to share it with Behat CLI.
@ -360,7 +348,7 @@ It is possible to include your own fixtures, it is explained further.
### Why do tests pass in a fresh installation, but fail in my own project? ### Why do tests pass in a fresh installation, but fail in my own project?
Because we're testing the interface directly, any changes to the Because we're testing the interface directly, any changes to the
viewed elements have the potential to disrupt testing. viewed elements have the potential to disrupt testing.
By building a test database from scratch, we're trying to minimize this impact. By building a test database from scratch, we're trying to minimize this impact.
Some examples where things can go wrong nevertheless: Some examples where things can go wrong nevertheless:
@ -388,7 +376,7 @@ methods, it is possible to delay the step execution by adding the following step
And I put a breakpoint And I put a breakpoint
This will stop the execution of the tests until you press the return key in the This will stop the execution of the tests until you press the return key in the
terminal. This is very useful when you want to look at the error or developer console terminal. This is very useful when you want to look at the error or developer console
inside the browser or if you want to interact with the session page manually. inside the browser or if you want to interact with the session page manually.
@ -597,7 +585,7 @@ It's based on the `vendor/bin/behat -di @cms` output.
Given /^I (?:press|follow) the "([^"]*)" (?:button|link), confirming the dialog$/ Given /^I (?:press|follow) the "([^"]*)" (?:button|link), confirming the dialog$/
Given /^I (?:press|follow) the "([^"]*)" (?:button|link), dismissing the dialog$/ Given /^I (?:press|follow) the "([^"]*)" (?:button|link), dismissing the dialog$/
Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, confirming the dialog$/ Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, confirming the dialog$/
Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, dismissing the dialog$/ Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, dismissing the dialog$/
@ -622,7 +610,7 @@ It's based on the `vendor/bin/behat -di @cms` output.
### CMS UI ### CMS UI
Then /^I should see an edit page form$/ Then /^I should see an edit page form$/
Then /^I should see the CMS$/ Then /^I should see the CMS$/
Then /^I should see a "([^"]*)" notice$/ Then /^I should see a "([^"]*)" notice$/
@ -679,7 +667,7 @@ It's based on the `vendor/bin/behat -di @cms` output.
Given /^(?:(an|a|the) )"group" "(?<id>[^"]+)" (?:(with|has)) permissions (?<permissionStr>.*)$/ Given /^(?:(an|a|the) )"group" "(?<id>[^"]+)" (?:(with|has)) permissions (?<permissionStr>.*)$/
- Example: Given a "group" "Admin" with permissions "Access to 'Pages' section" and "Access to 'Files' section" - Example: Given a "group" "Admin" with permissions "Access to 'Pages' section" and "Access to 'Files' section"
Given /^I assign (?:(an|a|the) )"(?<type>[^"]+)" "(?<value>[^"]+)" to (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)"$/ Given /^I assign (?:(an|a|the) )"(?<type>[^"]+)" "(?<value>[^"]+)" to (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)"$/
- Example: I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1" - Example: I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1"
@ -698,7 +686,7 @@ It's based on the `vendor/bin/behat -di @cms` output.
### Email ### Email
Given /^there should (not |)be an email (to|from) "([^"]*)"$/ Given /^there should (not |)be an email (to|from) "([^"]*)"$/
Given /^there should (not |)be an email (to|from) "([^"]*)" titled "([^"]*)"$/ Given /^there should (not |)be an email (to|from) "([^"]*)" titled "([^"]*)"$/
Given /^the email should (not |)contain "([^"]*)"$/ Given /^the email should (not |)contain "([^"]*)"$/
@ -723,7 +711,7 @@ It's based on the `vendor/bin/behat -di @cms` output.
When /^I click on the http link "([^"]*)" in the email$/ When /^I click on the http link "([^"]*)" in the email$/
- Example: When I click on the http link "http://localhost/changepassword" in the email - Example: When I click on the http link "http://localhost/changepassword" in the email
### Transformations ### Transformations
Behat [transformations](http://docs.behat.org/guides/2.definitions.html#step-argument-transformations) Behat [transformations](http://docs.behat.org/guides/2.definitions.html#step-argument-transformations)
@ -731,8 +719,8 @@ have the ability to change step arguments based on their original value,
for example to cast any argument matching the `\d` regex into an actual PHP integer. for example to cast any argument matching the `\d` regex into an actual PHP integer.
* `/^(?:(the|a)) time of (?<val>.*)$/`: Transforms relative time statements compatible with [strtotime()](http://www.php.net/manual/en/datetime.formats.relative.php). Example: "the time of 1 hour ago" might return "22:00:00" if its currently "23:00:00". * `/^(?:(the|a)) time of (?<val>.*)$/`: Transforms relative time statements compatible with [strtotime()](http://www.php.net/manual/en/datetime.formats.relative.php). Example: "the time of 1 hour ago" might return "22:00:00" if its currently "23:00:00".
* `/^(?:(the|a)) date of (?<val>.*)$/`: Transforms relative date statements compatible with [strtotime()](http://www.php.net/manual/en/datetime.formats.relative.php). Example: "the date of 2 days ago" might return "2013-10-10" if its currently the 12th of October 2013. * `/^(?:(the|a)) date of (?<val>.*)$/`: Transforms relative date statements compatible with [strtotime()](http://www.php.net/manual/en/datetime.formats.relative.php). Example: "the date of 2 days ago" might return "2013-10-10" if its currently the 12th of October 2013.
* `/^(?:(the|a)) datetime of (?<val>.*)$/`: Transforms relative date and time statements compatible with [strtotime()](http://www.php.net/manual/en/datetime.formats.relative.php). Example: "the datetime of 2 days ago" might return "2013-10-10 23:00:00" if its currently the 12th of October 2013. * `/^(?:(the|a)) datetime of (?<val>.*)$/`: Transforms relative date and time statements compatible with [strtotime()](http://www.php.net/manual/en/datetime.formats.relative.php). Example: "the datetime of 2 days ago" might return "2013-10-10 23:00:00" if its currently the 12th of October 2013.
## Useful resources ## Useful resources

View File

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

29
config/silverstripe.yml Normal file
View File

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

View File

@ -2,7 +2,7 @@
namespace SilverStripe\BehatExtension\Compiler; namespace SilverStripe\BehatExtension\Compiler;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Core\Manifest\ModuleLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
@ -26,11 +26,21 @@ class CoreInitializationPass implements CompilerPassInterface
require_once('Core/Core.php'); require_once('Core/Core.php');
// Include bootstrap file // Include bootstrap file
$bootstrapFile = $container->getParameter('behat.silverstripe_extension.bootstrap_file'); $bootstrapFile = $container->getParameter('silverstripe_extension.bootstrap_file');
if ($bootstrapFile) { if ($bootstrapFile) {
require_once $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']); unset($_GET['flush']);
// Remove the error handler so that PHPUnit can add its own // Remove the error handler so that PHPUnit can add its own

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

164
src/Extension.php Normal file
View File

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

23
src/MinkExtension.php Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@
namespace SilverStripe\BehatExtension\Utility; namespace SilverStripe\BehatExtension\Utility;
use SilverStripe\Dev\TestMailer as BaseTestMailer; use SilverStripe\Dev\TestMailer as BaseTestMailer;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\TestSession\TestSessionEnvironment; use SilverStripe\TestSession\TestSessionEnvironment;
/** /**
@ -13,7 +12,6 @@ use SilverStripe\TestSession\TestSessionEnvironment;
*/ */
class TestMailer extends BaseTestMailer class TestMailer extends BaseTestMailer
{ {
/** /**
* @var TestSessionEnvironment * @var TestSessionEnvironment
*/ */
@ -36,17 +34,6 @@ class TestMailer extends BaseTestMailer
$this->testSessionEnvironment->applyState($state); $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) public function findEmail($to = null, $from = null, $subject = null, $content = null)
{ {
$matches = $this->findEmails($to, $from, $subject, $content); $matches = $this->findEmails($to, $from, $subject, $content);

View File

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

View File

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

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

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

View File

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

View File

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