Compare commits
111 Commits
3.0.0-beta
...
4
Author | SHA1 | Date |
---|---|---|
Guy Sartorelli | 5c69f38876 | |
Steve Boyd | 1452c35e08 | |
github-actions | 6c9692a4b6 | |
Scott Sutherland | d40fa9848f | |
github-actions | 0d3e4a91cb | |
Sabina Talipova | 6c962ef294 | |
Steve Boyd | 5ea2f157b0 | |
Guy Sartorelli | 5c4a04864f | |
Steve Boyd | 9669e6b671 | |
Guy Sartorelli | e2c64133e5 | |
Steve Boyd | d1a7b36746 | |
Maxime Rainville | 1f5a17283e | |
Maime Rainville | 16d6814255 | |
Steve Boyd | fc6b47b821 | |
Maxime Rainville | dd2146fd65 | |
Sabina Talipova | b3de55e205 | |
Steve Boyd | aa26c912ce | |
Maxime Rainville | 3c1896be68 | |
Steve Boyd | 6e9582b0ea | |
Sabina Talipova | 2c425e7b8c | |
Steve Boyd | 5c79d79f78 | |
Sabina Talipova | 7c1762b36f | |
Steve Boyd | 4dcea617d8 | |
Guy Sartorelli | 276c60c75e | |
Steve Boyd | 5c6b9756fb | |
Steve Boyd | 6a0185d477 | |
Guy Sartorelli | 5aec7db4c5 | |
Sabina Talipova | 4e00b8ee95 | |
Steve Boyd | c012736293 | |
Steve Boyd | 6956470cfc | |
Guy Sartorelli | 567b1302b9 | |
Steve Boyd | 706ee2be9d | |
Steve Boyd | e5e6c7256a | |
Guy Sartorelli | 7bb03997cf | |
Steve Boyd | c460781971 | |
Steve Boyd | 984c1fbd04 | |
Steve Boyd | 754b5e0969 | |
Guy Sartorelli | a2fb05bd8f | |
Steve Boyd | 68306cd7a8 | |
Guy Sartorelli | ced518e890 | |
Guy Sartorelli | 5257536915 | |
Guy Sartorelli | e35b60d940 | |
Steve Boyd | 1f9fc6db05 | |
Tim Oliver | 261f88dd19 | |
Steve Boyd | 8ff1ef7a59 | |
Maxime Rainville | dcfa8ed1cd | |
Steve Boyd | c7801bb1fb | |
Daniel Hensby | e2deed514e | |
Steve Boyd | 92760462ac | |
Steve Boyd | efcdfb9c46 | |
Steve Boyd | b9b4c1d2a2 | |
Steve Boyd | bc76e19d5d | |
Maxime Rainville | 0939a30b12 | |
Steve Boyd | c7563fda09 | |
Maxime Rainville | 6ccf840e58 | |
Steve Boyd | 9ee6858f91 | |
Maxime Rainville | ba2e93131e | |
Steve Boyd | 76431b1190 | |
Maxime Rainville | 91be987eac | |
Steve Boyd | c754d70042 | |
Steve Boyd | d7321417b4 | |
Maxime Rainville | bcecdabbdf | |
Steve Boyd | 420ceb8c0d | |
Steve Boyd | fc9315123e | |
Maxime Rainville | efdd90b197 | |
Serge Latyntsev | 51455664fa | |
Steve Boyd | bc581dc248 | |
Steve Boyd | 4d6bd1890c | |
Ingo Schommer | fe0818afc2 | |
Garion Herman | d90f50fe10 | |
Serge Latyntcev | 892aee9592 | |
Daniel Hensby | 8dc02fd0db | |
Steve Boyd | b4ae738fe0 | |
Serge Latyntsev | 8940d2882d | |
Steve Boyd | bae6d30561 | |
Maxime Rainville | 6df2a983e3 | |
Aaron Carlino | a1d77988ce | |
Serge Latyntcev | e218fb85df | |
Robbie Averill | bd4a737833 | |
Maxime Rainville | a594f44188 | |
Maxime Rainville | 09cf3ca916 | |
Maxime Rainville | f94ff57604 | |
Robbie Averill | 6ffbd879dc | |
Serge Latyntcev | 64a4cd7469 | |
Maxime Rainville | 49100b5183 | |
Maxime Rainville | 74d8f3514e | |
Maxime Rainville | f02fd0c11f | |
Serge Latyntcev | 52573518bd | |
Robbie Averill | 94dca0e56a | |
Robbie Averill | a7c076fee0 | |
Robbie Averill | 5146e6fac0 | |
Guy Marriott | e892ef4829 | |
Robbie Averill | 5ea8aae96d | |
Guy Marriott | b8d658114b | |
Guy Marriott | 2cbcab20c1 | |
Robbie Averill | 138456e497 | |
Raissa North | 7c8ba06522 | |
Damian Mooyman | 894f7d5199 | |
Dylan Wagstaff | 1de04743d5 | |
Damian Mooyman | 55e9221dcd | |
Damian Mooyman | 6ef427f467 | |
Damian Mooyman | 3cf7db107b | |
Damian Mooyman | 42efff5eac | |
Damian Mooyman | 5fdab55ebf | |
Damian Mooyman | 04789ceac1 | |
Damian Mooyman | 81a45e2819 | |
Damian Mooyman | b403d502da | |
Damian Mooyman | 4e36ec793e | |
Christopher Joe | 1d3da4cd2f | |
Chris Joe | 493ccb05cd | |
Damian Mooyman | 4657c39e24 |
|
@ -14,6 +14,9 @@ trim_trailing_whitespace = true
|
|||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[composer.json]
|
||||
indent_size = 4
|
||||
|
||||
# The indent size used in the package.json file cannot be changed:
|
||||
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: CI
|
||||
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1
|
|
@ -0,0 +1,16 @@
|
|||
name: Dispatch CI
|
||||
|
||||
on:
|
||||
# At 2:10 PM UTC, only on Sunday and Monday
|
||||
schedule:
|
||||
- cron: '10 14 * * 0,1'
|
||||
|
||||
jobs:
|
||||
dispatch-ci:
|
||||
name: Dispatch CI
|
||||
# Only run cron on the silverstripe account
|
||||
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dispatch CI
|
||||
uses: silverstripe/gha-dispatch-ci@v1
|
|
@ -0,0 +1,17 @@
|
|||
name: Keepalive
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# The 8th of every month at 2:50pm UTC
|
||||
schedule:
|
||||
- cron: '50 14 8 * *'
|
||||
|
||||
jobs:
|
||||
keepalive:
|
||||
name: Keepalive
|
||||
# Only run cron on the silverstripe account
|
||||
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Keepalive
|
||||
uses: silverstripe/gha-keepalive@v1
|
39
.travis.yml
39
.travis.yml
|
@ -1,39 +0,0 @@
|
|||
language: php
|
||||
|
||||
dist: precise
|
||||
|
||||
sudo: false
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache/files
|
||||
|
||||
php:
|
||||
- 5.6
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- PHPUNIT_TEST=1
|
||||
- PHPCS_TEST=1
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- php: 5.6
|
||||
env: PHPUNIT_TEST=1
|
||||
- php: 7.0
|
||||
env: PHPUNIT_TEST=1
|
||||
- php: 7.1.2
|
||||
env: PHPUNIT_TEST=1
|
||||
|
||||
before_script:
|
||||
- export PATH=~/.composer/vendor/bin:$PATH
|
||||
- composer validate
|
||||
- composer install --dev --prefer-dist
|
||||
- composer require --prefer-dist --no-update silverstripe/recipe-core:1.0.x-dev
|
||||
- composer update
|
||||
- if [[ $PHPCS_TEST ]]; then composer global require squizlabs/php_codesniffer:^3 --prefer-dist --no-interaction --no-progress --no-suggest -o; fi
|
||||
- phpenv rehash
|
||||
|
||||
script:
|
||||
- if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit tests/php; fi
|
||||
- if [[ $PHPCS_TEST ]]; then composer run-script lint; fi
|
148
README.md
148
README.md
|
@ -1,14 +1,15 @@
|
|||
# SilverStripe Integration for Behat
|
||||
# Silverstripe Integration for Behat
|
||||
|
||||
[![Build Status](https://travis-ci.org/silverstripe-labs/silverstripe-behat-extension.svg?branch=master)](https://travis-ci.org/silverstripe-labs/silverstripe-behat-extension)
|
||||
[![CI](https://github.com/silverstripe/silverstripe-behat-extension/actions/workflows/ci.yml/badge.svg)](https://github.com/silverstripe/silverstripe-behat-extension/actions/workflows/ci.yml)
|
||||
[![Silverstripe supported module](https://img.shields.io/badge/silverstripe-supported-0071C4.svg)](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/)
|
||||
|
||||
## Overview
|
||||
|
||||
[Behat](http://behat.org) is a testing framework for behaviour-driven development.
|
||||
Because it primarily interacts with your website through a browser,
|
||||
you don't need any specific integration tools to get it going with
|
||||
a basic SilverStripe website, simply follow the
|
||||
[standard Behat usage instructions](http://docs.behat.org/).
|
||||
a basic Silverstripe website, simply follow the
|
||||
[standard Behat usage instructions](http://docs.behat.org/en/latest/user_guide.html).
|
||||
|
||||
This extension comes in handy if you want to go beyond
|
||||
interacting with an existing website and database,
|
||||
|
@ -17,7 +18,7 @@ would need to be rolled back to a "clean slate" later on.
|
|||
|
||||
It provides the following helpers:
|
||||
|
||||
* Provide access to SilverStripe classes in your Behat contexts
|
||||
* Provide access to Silverstripe classes in your Behat contexts
|
||||
* Set up a temporary database automatically
|
||||
* Reset the database content on every scenario
|
||||
* Prebuilt Contexts for SilverStripe's login forms and other common tasks
|
||||
|
@ -29,7 +30,7 @@ It provides the following helpers:
|
|||
|
||||
In order to achieve this, the extension makes one basic assumption:
|
||||
Your Behat tests are run from the same application as the tested
|
||||
SilverStripe codebase, on a locally hosted website from the same codebase.
|
||||
Silverstripe codebase, on a locally hosted website from the same codebase.
|
||||
This is important because we need access to the underlying SilverStripe
|
||||
PHP classes. You can of course use a remote browser to do the actual testing.
|
||||
|
||||
|
@ -37,56 +38,35 @@ Note: The extension has only been tested with the `selenium2` Mink driver.
|
|||
|
||||
## Installation
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
cd my-test-project
|
||||
composer require --dev silverstripe/behat-extension:"@stable"
|
||||
composer require --dev silverstripe/behat-extension
|
||||
|
||||
Now get the latest Selenium2 server (requires [Java](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)):
|
||||
Download the standalone [Google Chrome WebDriver](http://chromedriver.storage.googleapis.com/index.html?path=2.8/)
|
||||
|
||||
composer require --dev se/selenium-server-standalone:"2.x@stable"
|
||||
|
||||
Download [Firefox 31.0 ESR](https://ftp.mozilla.org/pub/firefox/releases/31.8.0esr/) (Extended Support Release).
|
||||
This version is 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). Even newer Firefox ESR versions are likely to break with the Selenium version we're running.
|
||||
|
||||
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
|
||||
[`_ss_environment.php`](http://doc.silverstripe.org/framework/en/topics/environment-management)
|
||||
file one level above the webroot.
|
||||
[`.env`](https://docs.silverstripe.org/en/getting_started/environment_management/) file one level above the webroot.
|
||||
|
||||
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
|
||||
Unless you have [`SS_BASE_URL`](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
|
||||
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/"
|
||||
|
||||
## Usage
|
||||
|
||||
### Prevent Firefox from Automatically Updating
|
||||
|
||||
The moment you open Firefox, it's going to try and update itself out of the stone age. To prevent this, open a new tab and go to `about:config`. There, change the following settings to `false`:
|
||||
|
||||
- `app.update.auto`
|
||||
- `app.update.enabled`
|
||||
- `app.update.silent`
|
||||
|
||||
Firefox will already have started the update, so close and delete it. The settings you changed should be stored as preferences, apart from the application files you've just deleted. Reinstall that ancient version. The next time you open it, and go to "About Firefox", you should see a button desperately pleading with you to "check for updates". Don't click that if you know what's good for you...
|
||||
|
||||
### Starting the Selenium Server
|
||||
### Starting ChromeDriver
|
||||
|
||||
You can run the server locally in a separate Terminal session:
|
||||
|
||||
vendor/bin/selenium-server-standalone
|
||||
|
||||
In some cases it may be necessary to start a specific version of Firefox
|
||||
|
||||
vendor/bin/selenium-server-standalone -Dwebdriver.firefox.bin="/Applications/Firefox31.app/Contents/MacOS/firefox-bin"
|
||||
chromedriver
|
||||
|
||||
### Running the Tests
|
||||
|
||||
|
@ -98,9 +78,23 @@ Or even run a single scenario by it's name (supports regular expressions):
|
|||
|
||||
vendor/bin/behat --name 'My scenario title' @framework
|
||||
|
||||
This will start a Firefox browser by default. Other browsers and profiles can be configured in `behat.yml`.
|
||||
This will start a Chrome browser by default. Other browsers and profiles can be configured in `behat.yml`.
|
||||
|
||||
For example, if you want to start a Chrome Browser you can following the instructions provided [here](docs/chrome-behat.md).
|
||||
### Running with stand-alone command (requires Bash)
|
||||
|
||||
If running with `silverstripe/serve` and `chromedriver`, you can also use the following command
|
||||
which will automatically start and stop these services for individual tests.
|
||||
|
||||
vendor/bin/behat-ss @framework
|
||||
|
||||
This automates:
|
||||
- starting server
|
||||
- starting chromedriver
|
||||
- running behat
|
||||
- shutting down chromedriver
|
||||
- shutting down server
|
||||
|
||||
Make sure you set `SS_BASE_URL` to `http://localhost:8080` in `.env`
|
||||
|
||||
## Tutorials
|
||||
|
||||
|
@ -110,18 +104,18 @@ For example, if you want to start a Chrome Browser you can following the instruc
|
|||
|
||||
## Configuration
|
||||
|
||||
The SilverStripe installer already comes with a YML configuration
|
||||
which is ready to run tests on a locally hosted Selenium server,
|
||||
The Silverstripe installer already comes with a YML configuration
|
||||
which is ready to run tests on the standalone ChromeDriver server,
|
||||
located in the project root as `behat.yml`.
|
||||
|
||||
You should ensure that you have configured SS_BASE_URL in your `.env`.
|
||||
You should ensure that you have configured `SS_BASE_URL` in your `.env` file.
|
||||
|
||||
Generic Mink configuration settings are placed in `SilverStripe\BehatExtension\MinkExtension`,
|
||||
which is a subclass of `Behat\MinkExtension\Extension`.
|
||||
|
||||
Overview of settings (all in the `extensions.SilverStripe\BehatExtension\Extension` path):
|
||||
|
||||
* `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
|
||||
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.
|
||||
|
@ -151,14 +145,15 @@ Example: behat.yml
|
|||
- %paths.modules.framework%/tests/behat/features/files/
|
||||
extensions:
|
||||
SilverStripe\BehatExtension\MinkExtension:
|
||||
default_session: selenium2
|
||||
javascript_session: selenium2
|
||||
selenium2:
|
||||
browser: firefox
|
||||
default_session: facebook_web_driver
|
||||
javascript_session: facebook_web_driver
|
||||
facebook_web_driver:
|
||||
browser: chrome
|
||||
wd_host: "http://127.0.0.1:9515" #chromedriver port
|
||||
SilverStripe\BehatExtension\Extension:
|
||||
screenshot_path: %paths.base%/artifacts/screenshots
|
||||
|
||||
## Module Initialization
|
||||
## Module Initialisation
|
||||
|
||||
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
|
||||
|
@ -169,7 +164,7 @@ 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
|
||||
(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.
|
||||
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:
|
||||
|
||||
vendor/bin/behat --init @mymodule --namespace="MyVendor\MyModule"
|
||||
|
@ -189,7 +184,7 @@ To find out all available steps (and the files they are defined in), run the fol
|
|||
|
||||
vendor/bin/behat @mymodule --definitions=i
|
||||
|
||||
Note: There are more specific step definitions in the SilverStripe `framework` module
|
||||
Note: There are more specific step definitions in the Silverstripe `framework` module
|
||||
for interacting with the CMS interfaces (see `framework/tests/behat/features/bootstrap`).
|
||||
In addition to the dynamic list, a cheatsheet of available steps can be found at the end of this guide.
|
||||
|
||||
|
@ -240,10 +235,11 @@ use the inline definition syntax. The following example shows some syntax variat
|
|||
Scenario: View a page in the tree
|
||||
Given I am logged in with "ADMIN" permissions
|
||||
And I go to "/admin/pages"
|
||||
Then I should see "Page 1" in CMS Tree
|
||||
Then I should see "Page 1"
|
||||
|
||||
* 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/en/latest/user_guide/writing_scenarios.html#backgrounds).
|
||||
If you want them to be created only when a particular scenario runs, define them there.
|
||||
* Fixtures are cleared between scenarios.
|
||||
* The basic syntax works for all `DataObject` subclasses, but some specific
|
||||
|
@ -260,7 +256,7 @@ use the inline definition syntax. The following example shows some syntax variat
|
|||
|
||||
### Directory Structure
|
||||
|
||||
As a convention, SilverStripe Behat tests live in a `tests/behat` subfolder
|
||||
As a convention, Silverstripe Behat tests live in a `tests/behat` subfolder
|
||||
of your module. You can create it with the following commands:
|
||||
|
||||
mkdir -p mymodule/tests/behat/features/
|
||||
|
@ -268,7 +264,7 @@ of your module. You can create it with the following commands:
|
|||
|
||||
### FeatureContext
|
||||
|
||||
The generic [Behat usage instructions](http://docs.behat.org/) apply
|
||||
The generic [Behat usage instructions](http://docs.behat.org/en/latest/user_guide.html) apply
|
||||
here as well. The only major difference is the base class from which
|
||||
to extend your own `FeatureContext`: It should be `SilverStripeContext`
|
||||
rather than `BehatContext`.
|
||||
|
@ -331,12 +327,10 @@ check `BasicContext->handleAjaxBeforeStep()` and the `ajax_steps` configuration
|
|||
|
||||
### Why does the module need to know about the framework path on the filesystem?
|
||||
|
||||
Sometimes SilverStripe needs to know the URL of your site. When you're visiting
|
||||
Sometimes Silverstripe needs to know the URL of your site. When you're visiting
|
||||
your site in a web browser this is easy to work out, but if you're executing
|
||||
scripts on the command-line, it has no way of knowing.
|
||||
|
||||
To work this out, this module is using [file to URL mapping](http://doc.silverstripe.org/framework/en/topics/commandline#configuration).
|
||||
|
||||
### How does the module interact with the SS database?
|
||||
|
||||
The module creates temporary database on init and is switching to the alternative
|
||||
|
@ -354,7 +348,7 @@ viewed elements have the potential to disrupt testing.
|
|||
By building a test database from scratch, we're trying to minimize this impact.
|
||||
Some examples where things can go wrong nevertheless:
|
||||
|
||||
* Thirdparty SilverStripe modules which install default data
|
||||
* Thirdparty Silverstripe modules which install default data
|
||||
* Changes to the default interface language
|
||||
* Configurations which remove admin areas or specific fields
|
||||
|
||||
|
@ -366,7 +360,7 @@ or run tests on a "sandbox" projects without these modules.
|
|||
|
||||
First, read the console output. Behat will tell you which steps have failed.
|
||||
|
||||
SilverStripe Behaviour Testing Framework also notifies you about some events.
|
||||
Silverstripe Behaviour Testing Framework also notifies you about some events.
|
||||
It tries to catch some JavaScript errors and AJAX errors as well although it
|
||||
is limited to errors that occur after the page is loaded.
|
||||
|
||||
|
@ -405,13 +399,8 @@ The `macgdbp` IDE key needs to match your `xdebug.idekey` php.ini setting.
|
|||
### How do I set up continuous integration through Travis?
|
||||
|
||||
Check out the [travis.yml](https://github.com/silverstripe/silverstripe-framework/blob/master/.travis.yml)
|
||||
in `silverstripe/framework` for a good example on how to set up Behat tests through [travis-ci.org](http://travis-ci.org).
|
||||
Note that the [Travis CI Environment](https://docs.travis-ci.com/user/ci-environment#Firefox)
|
||||
does not default to the latest [Firefox ESR](https://www.mozilla.org/en-US/firefox/organizations/all/) release, but an older version.
|
||||
(31.0 in January 2017). You should try to run your tests locally with the same Firefox version,
|
||||
and [download the correct Firefox release](https://ftp.mozilla.org/pub/firefox/releases/)).
|
||||
Alternatively, [configure Travis](https://docs.travis-ci.com/user/firefox/) to use a newer version.
|
||||
Don't forget to disable auto-updates in your Firefox settings.
|
||||
in `silverstripe/framework` for a good example on how to set up Behat tests through
|
||||
[travis-ci.org](http://travis-ci.org).
|
||||
|
||||
## Cheatsheet
|
||||
|
||||
|
@ -635,7 +624,6 @@ It's based on the `vendor/bin/behat -di @cms` output.
|
|||
|
||||
Given /^the preview does not contain "([^"]*)"$/
|
||||
|
||||
|
||||
### Fixtures
|
||||
|
||||
Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" (:?which )?redirects to (?:(an|a|the) )"(?<targetType>[^"]+)" "(?<targetId>[^"]+)"$/
|
||||
|
@ -645,7 +633,7 @@ It's based on the `vendor/bin/behat -di @cms` output.
|
|||
- Example: Given a "page" "Page 1"
|
||||
|
||||
Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" with (?<data>.*)$/
|
||||
- Example: Given a "page" "Page 1" with "URL"="page-1" and "Content"="my page 1"
|
||||
- Example: Given a "page" "Page 1" with "URLSegment"="page-1" and "Content"="my page 1"
|
||||
|
||||
Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" has the following data$/
|
||||
- Example: And the "page" "Page 2" has the following data
|
||||
|
@ -660,7 +648,7 @@ It's based on the `vendor/bin/behat -di @cms` output.
|
|||
- Example: Given the "page" "Page 1" is deleted
|
||||
|
||||
Given /^there are the following ([^\s]*) records$/
|
||||
- Accepts YAML fixture definitions similar to the ones used in SilverStripe unit testing.
|
||||
- Accepts YAML fixture definitions similar to the ones used in Silverstripe unit testing.
|
||||
|
||||
Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)"$/
|
||||
- Example: Given a "member" "Admin" belonging to "Admin Group"
|
||||
|
@ -678,7 +666,7 @@ It's based on the `vendor/bin/behat -di @cms` output.
|
|||
|
||||
Given /^the CMS settings have the following data$/
|
||||
- Example: Given the CMS settings has the following data
|
||||
- Note: It only works with the SilverStripe CMS module installed
|
||||
- Note: It only works with the Silverstripe CMS module installed
|
||||
|
||||
### Environment
|
||||
|
||||
|
@ -716,16 +704,22 @@ It's based on the `vendor/bin/behat -di @cms` output.
|
|||
|
||||
### Transformations
|
||||
|
||||
Behat [transformations](http://docs.behat.org/guides/2.definitions.html#step-argument-transformations)
|
||||
Behat [transformations](http://docs.behat.org/en/v2.5/guides/2.definitions.html#step-argument-transformations)
|
||||
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.
|
||||
|
||||
* `/^(?:(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)) 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)) 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)) 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
|
||||
|
||||
* [SilverStripe CMS architecture](http://doc.silverstripe.org/sapphire/en/trunk/reference/cms-architecture)
|
||||
* [SilverStripe Framework Test Module](https://github.com/silverstripe-labs/silverstripe-frameworktest)
|
||||
* [SilverStripe Unit and Integration Testing](http://doc.silverstripe.org/sapphire/en/trunk/topics/testing)
|
||||
* [Silverstripe CMS architecture](http://doc.silverstripe.org/sapphire/en/trunk/reference/cms-architecture)
|
||||
* [Silverstripe Framework Test Module](https://github.com/silverstripe-labs/silverstripe-frameworktest)
|
||||
* [Silverstripe Unit and Integration Testing](http://doc.silverstripe.org/sapphire/en/trunk/topics/testing)
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/sh
|
||||
echo "setting up /artifacts"
|
||||
mkdir -p artifacts
|
||||
|
||||
echo "starting chromedriver"
|
||||
chromedriver &> artifacts/chromedriver.log 2> artifacts/chromedriver-error.log &
|
||||
cd_pid=$!
|
||||
|
||||
echo "starting webserver"
|
||||
vendor/bin/serve &> artifacts/serve.log 2> artifacts/serve-error.log &
|
||||
ws_pid=$!
|
||||
|
||||
echo "starting behat"
|
||||
vendor/bin/behat "$@"
|
||||
|
||||
echo "killing webserver (PID: $ws_pid)"
|
||||
pkill -TERM -P $ws_pid &> /dev/null
|
||||
|
||||
echo "killing chromedriver (PID: $cd_pid)"
|
||||
kill -9 $cd_pid &> /dev/null
|
61
build.php
61
build.php
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
$filename = 'silverstripe_extension.phar';
|
||||
|
||||
if (file_exists($filename)) {
|
||||
unlink($filename);
|
||||
}
|
||||
|
||||
$phar = new \Phar($filename, 0, 'extension.phar');
|
||||
$phar->setSignatureAlgorithm(\Phar::SHA1);
|
||||
$phar->startBuffering();
|
||||
|
||||
foreach (findFiles('src') as $path) {
|
||||
$phar->addFromString($path, file_get_contents(__DIR__ . '/' . $path));
|
||||
}
|
||||
|
||||
$phar->addFromString('init.php', file_get_contents(__DIR__ . '/init.php'));
|
||||
|
||||
$phar->setStub(<<<STUB
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the SilverStripe-Behaviour-Testing-Framework
|
||||
*
|
||||
* (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.
|
||||
*/
|
||||
|
||||
Phar::mapPhar('extension.phar');
|
||||
|
||||
return require 'phar://extension.phar/init.php';
|
||||
|
||||
__HALT_COMPILER();
|
||||
STUB
|
||||
);
|
||||
$phar->stopBuffering();
|
||||
|
||||
function findFiles($dir)
|
||||
{
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::CHILD_FIRST);
|
||||
|
||||
$files = array();
|
||||
foreach ($iterator as $path) {
|
||||
if ($path->isFile()) {
|
||||
$files[] = $path->getPath() . DIRECTORY_SEPARATOR . $path->getFilename();
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
102
composer.json
102
composer.json
|
@ -1,59 +1,51 @@
|
|||
{
|
||||
"name": "silverstripe/behat-extension",
|
||||
"type": "behat-extension",
|
||||
"description": "SilverStripe framework extension for Behat",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"web",
|
||||
"bdd",
|
||||
"silverstripe"
|
||||
],
|
||||
"homepage": "http://silverstripe.org",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michal Ochman",
|
||||
"email": "ochman.d.michal@gmail.com"
|
||||
"name": "silverstripe/behat-extension",
|
||||
"type": "behat-extension",
|
||||
"description": "SilverStripe framework extension for Behat",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"web",
|
||||
"bdd",
|
||||
"silverstripe"
|
||||
],
|
||||
"homepage": "http://silverstripe.org",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michal Ochman",
|
||||
"email": "ochman.d.michal@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Ingo Schommer",
|
||||
"email": "ingo@silverstripe.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"squizlabs/php_codesniffer": "^3",
|
||||
"behat/behat": "^3.9",
|
||||
"behat/mink": "^1.7",
|
||||
"behat/mink-extension": "^2.1",
|
||||
"silverstripe/mink-facebook-web-driver": "^1",
|
||||
"symfony/dom-crawler": "^3 || ^4",
|
||||
"silverstripe/testsession": "^2.2",
|
||||
"silverstripe/framework": "^4.10",
|
||||
"symfony/finder": "^3.2 || ^4"
|
||||
},
|
||||
{
|
||||
"name": "Ingo Schommer",
|
||||
"email": "ingo@silverstripe.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.6",
|
||||
"phpunit/phpunit": "^5.7",
|
||||
"behat/behat": "^3.2",
|
||||
"behat/mink": "^1.7",
|
||||
"behat/mink-extension": "^2.1",
|
||||
"behat/mink-selenium2-driver": "^1.3",
|
||||
"symfony/dom-crawler": "^3",
|
||||
"silverstripe/testsession": "^2.0.0@alpha",
|
||||
"silverstripe/framework": "^4@dev",
|
||||
"symfony/finder": "^3.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SilverStripe\\BehatExtension\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"SilverStripe\\BehatExtension\\Tests\\": "tests/php/"
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SilverStripe\\BehatExtension\\": "src/",
|
||||
"SilverStripe\\BehatExtension\\Tests\\": "tests/php/"
|
||||
}
|
||||
},
|
||||
"classmap": [
|
||||
"framework",
|
||||
"vendor/phpunit/phpunit"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "phpcs --standard=PSR2 -n src/ tests/php/"
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"minimum-stability": "dev"
|
||||
"extra": [],
|
||||
"bin": [
|
||||
"bin/behat-ss"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "phpcs --standard=PSR2 -n src/ tests/php/"
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ parameters:
|
|||
silverstripe_extension.ajax_steps: ~
|
||||
silverstripe_extension.ajax_timeout: ~
|
||||
silverstripe_extension.admin_url: ~
|
||||
silverstripe_extension.is_ci: ~
|
||||
silverstripe_extension.login_url: ~
|
||||
silverstripe_extension.screenshot_path: ~
|
||||
silverstripe_extension.module:
|
||||
|
@ -17,13 +18,13 @@ parameters:
|
|||
silverstripe_extension.context.class_path: tests/behat/src/
|
||||
services:
|
||||
silverstripe_extension.context.initializer:
|
||||
class: %silverstripe_extension.context.initializer.class%
|
||||
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%]]
|
||||
- [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 }
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
#Running Behat Tests using Chrome
|
||||
If you would like to run Behat Tests using Google Chrome here are a few steps I went through to get that setup.
|
||||
|
||||
1) [Download the Google Chrome Webdriver](http://chromedriver.storage.googleapis.com/index.html?path=2.8/)
|
||||
|
||||
2) Unzip the file, and place the chromedriver file in a known location.
|
||||
|
||||
3) Now edit the `behat.yml` file and create a new profile for using Chrome by adding the following below the default profile.
|
||||
|
||||
```
|
||||
default_session: selenium2
|
||||
javascript_session: selenium2
|
||||
selenium2:
|
||||
browser: chrome
|
||||
SilverStripe\BehatExtension\Extension:
|
||||
screenshot_path: %behat.paths.base%/_artifacts/screenshots
|
||||
```
|
||||
|
||||
4) Now we need to use the new webdriver with Selenium.
|
||||
|
||||
```
|
||||
java -jar selenium-server.jar -Dwebdriver.chrome.driver="/path/to/chromedriver"
|
||||
```
|
||||
|
||||
5) Now run your behat steps with the chrome profile.
|
||||
|
||||
```
|
||||
behat @mysite --profile=chrome
|
||||
```
|
|
@ -21,7 +21,7 @@ First of all, check out a default SilverStripe project
|
|||
and ensure it runs on your environment. Detailed installation instructions
|
||||
can be found in the [README](../README.md) of this module.
|
||||
Once you've got the SilverStripe project running, make sure you've
|
||||
started Selenium. With all configuration in place, initialize Behat
|
||||
started ChromeDriver. With all configuration in place, initialise Behat
|
||||
for
|
||||
|
||||
vendor/bin/behat --init @mysite
|
||||
|
@ -246,7 +246,7 @@ not exactly one record found in the relation, and hence fail that step for Behat
|
|||
public function thereShouldBeAnAbuseReportForWithReason($id, $reason)
|
||||
{
|
||||
$page = $this->fixtureFactory->get('Page', $id);
|
||||
assertEquals(1, $page->PageAbuseReports()->filter('Reason', $reason)->Count());
|
||||
Assert::assertEquals(1, $page->PageAbuseReports()->filter('Reason', $reason)->Count());
|
||||
}
|
||||
```
|
||||
|
||||
|
|
19
init.php
19
init.php
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
spl_autoload_register(function ($class) {
|
||||
if (false !== strpos($class, 'SilverStripe\\BehatExtension')) {
|
||||
require_once(__DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php');
|
||||
return true;
|
||||
}
|
||||
}, true, false);
|
||||
|
||||
return new SilverStripe\BehatExtension\Extension;
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset name="SilverStripe">
|
||||
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
|
||||
|
||||
<file>src</file>
|
||||
<file>tests</file>
|
||||
|
||||
<!-- base rules are PSR-2 -->
|
||||
<rule ref="PSR2" >
|
||||
<!-- Current exclusions -->
|
||||
<exclude name="PSR1.Methods.CamelCapsMethodName" />
|
||||
<exclude name="PSR2.Classes.PropertyDeclaration.Underscore" />
|
||||
<exclude name="Squiz.Classes.ValidClassName.NotCamelCaps" />
|
||||
<exclude name="Generic.Files.LineLength.TooLong" />
|
||||
<exclude name="PSR1.Files.SideEffects.FoundWithSymbols" />
|
||||
</rule>
|
||||
</ruleset>
|
|
@ -1,5 +1,8 @@
|
|||
<phpunit colors="true">
|
||||
<testsuite name="Default">
|
||||
<directory>tests/php</directory>
|
||||
</testsuite>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="Default">
|
||||
<directory>tests/php</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
|
|
|
@ -7,6 +7,7 @@ use SilverStripe\Control\Controller;
|
|||
use SilverStripe\Control\HTTPApplication;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Core\CoreKernel;
|
||||
use SilverStripe\Core\Environment;
|
||||
use SilverStripe\Core\Manifest\ModuleLoader;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
@ -28,8 +29,8 @@ class CoreInitializationPass implements CompilerPassInterface
|
|||
file_put_contents('php://stdout', 'Bootstrapping' . PHP_EOL);
|
||||
|
||||
// Connect to database and build manifest
|
||||
if (!getenv('SS_ENVIRONMENT_TYPE')) {
|
||||
putenv('SS_ENVIRONMENT_TYPE=dev');
|
||||
if (!Environment::getEnv('SS_ENVIRONMENT_TYPE')) {
|
||||
Environment::setEnv('SS_ENVIRONMENT_TYPE', 'dev');
|
||||
}
|
||||
|
||||
// Include bootstrap file
|
||||
|
@ -62,7 +63,7 @@ class CoreInitializationPass implements CompilerPassInterface
|
|||
$container->setParameter('paths.modules.'.$module->getShortName(), $module->getPath());
|
||||
$composerName = $module->getComposerName();
|
||||
if ($composerName) {
|
||||
list($vendor,$name) = explode('/', $composerName);
|
||||
list($vendor,$name) = explode('/', $composerName ?? '');
|
||||
$container->setParameter('paths.modules.'.$vendor.'.'.$name, $module->getPath());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace SilverStripe\BehatExtension\Compiler;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\Core\Environment;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
|
||||
|
@ -23,7 +24,7 @@ class MinkExtensionBaseUrlPass implements CompilerPassInterface
|
|||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
// Set url from environment
|
||||
$baseURL = getenv('SS_BASE_URL');
|
||||
$baseURL = Environment::getEnv('SS_BASE_URL');
|
||||
if (!$baseURL) {
|
||||
throw new InvalidArgumentException(
|
||||
'"base_url" not configured. Please specify it in your .env config with SS_BASE_URL'
|
||||
|
|
|
@ -2,22 +2,31 @@
|
|||
|
||||
namespace SilverStripe\BehatExtension\Context;
|
||||
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Behat\Behat\Context\Context;
|
||||
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
|
||||
use Behat\Behat\Definition\Call;
|
||||
use Behat\Behat\Hook\Scope\AfterScenarioScope;
|
||||
use Behat\Behat\Hook\Scope\AfterStepScope;
|
||||
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
|
||||
use Behat\Behat\Hook\Scope\BeforeStepScope;
|
||||
use Behat\Behat\Hook\Scope\StepScope;
|
||||
use Behat\Mink\Driver\Selenium2Driver;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Behat\Mink\Exception\ElementNotFoundException;
|
||||
use Behat\Mink\Session;
|
||||
use Behat\Testwork\Tester\Result\TestResult;
|
||||
use Exception;
|
||||
use Facebook\WebDriver\Exception\WebDriverException;
|
||||
use Facebook\WebDriver\WebDriver;
|
||||
use Facebook\WebDriver\WebDriverAlert;
|
||||
use Facebook\WebDriver\WebDriverExpectedCondition;
|
||||
use Facebook\WebDriver\WebDriverKeys;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use SilverStripe\Assets\File;
|
||||
use SilverStripe\Assets\Filesystem;
|
||||
use SilverStripe\BehatExtension\Utility\StepHelper;
|
||||
use WebDriver\Exception as WebDriverException;
|
||||
use WebDriver\Session as WebDriverSession;
|
||||
use SilverStripe\BehatExtension\Utility\DebugTools;
|
||||
use SilverStripe\MinkFacebookWebDriver\FacebookWebDriver;
|
||||
|
||||
/**
|
||||
* BasicContext
|
||||
|
@ -31,6 +40,7 @@ class BasicContext implements Context
|
|||
{
|
||||
use MainContextAwareTrait;
|
||||
use StepHelper;
|
||||
use DebugTools;
|
||||
|
||||
/**
|
||||
* Date format in date() syntax
|
||||
|
@ -51,6 +61,50 @@ class BasicContext implements Context
|
|||
*/
|
||||
protected $datetimeFormat = 'Y-m-d H:i:s';
|
||||
|
||||
/**
|
||||
* @var FixtureContext
|
||||
*/
|
||||
protected $fixtureContext = null;
|
||||
|
||||
/**
|
||||
* Get the fixture context of the current module
|
||||
*
|
||||
* @BeforeScenario
|
||||
*/
|
||||
public function gatherContexts(BeforeScenarioScope $scope): void
|
||||
{
|
||||
/** @var InitializedContextEnvironment $environment */
|
||||
$environment = $scope->getEnvironment();
|
||||
|
||||
// Find the FixtureContext defined in behat.yml
|
||||
$subClasses = $this->getSubclassesOf(FixtureContext::class);
|
||||
foreach ($subClasses as $class) {
|
||||
if (!$environment->hasContextClass($class)) {
|
||||
continue;
|
||||
}
|
||||
$this->fixtureContext = $environment->getContext($class);
|
||||
break;
|
||||
}
|
||||
// Fallback to base FixtureClass
|
||||
if (!$this->fixtureContext && $environment->hasContextClass(FixtureContext::class)) {
|
||||
$this->fixtureContext = $environment->getContext(FixtureContext::class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the subclasses of a class
|
||||
*/
|
||||
private function getSubclassesOf($parent): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (get_declared_classes() as $class) {
|
||||
if (is_subclass_of($class, $parent ?? '')) {
|
||||
$result[] = $class;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Mink session from MinkContext
|
||||
*
|
||||
|
@ -125,7 +179,7 @@ JS;
|
|||
$jserrors = $page->find('xpath', '//body[@data-jserrors]');
|
||||
if (null !== $jserrors) {
|
||||
$this->takeScreenshot($event);
|
||||
file_put_contents('php://stderr', $jserrors->getAttribute('data-jserrors') . PHP_EOL);
|
||||
$this->logMessage($jserrors->getAttribute('data-jserrors'));
|
||||
}
|
||||
|
||||
$javascript = <<<JS
|
||||
|
@ -158,9 +212,9 @@ JS;
|
|||
}
|
||||
try {
|
||||
$ajaxEnabledSteps = $this->getMainContext()->getAjaxSteps();
|
||||
$ajaxEnabledSteps = implode('|', array_filter($ajaxEnabledSteps));
|
||||
$ajaxEnabledSteps = implode('|', array_filter($ajaxEnabledSteps ?? []));
|
||||
|
||||
if (empty($ajaxEnabledSteps) || !preg_match('/(' . $ajaxEnabledSteps . ')/i', $event->getStep()->getText())) {
|
||||
if (empty($ajaxEnabledSteps) || !preg_match('/(' . $ajaxEnabledSteps . ')/i', $event->getStep()->getText() ?? '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -211,9 +265,9 @@ JS;
|
|||
}
|
||||
try {
|
||||
$ajaxEnabledSteps = $this->getMainContext()->getAjaxSteps();
|
||||
$ajaxEnabledSteps = implode('|', array_filter($ajaxEnabledSteps));
|
||||
$ajaxEnabledSteps = implode('|', array_filter($ajaxEnabledSteps ?? []));
|
||||
|
||||
if (empty($ajaxEnabledSteps) || !preg_match('/(' . $ajaxEnabledSteps . ')/i', $event->getStep()->getText())) {
|
||||
if (empty($ajaxEnabledSteps) || !preg_match('/(' . $ajaxEnabledSteps . ')/i', $event->getStep()->getText() ?? '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -246,26 +300,6 @@ JS;
|
|||
$this->getSession()->wait(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take screenshot when step fails.
|
||||
* Works only with Selenium2Driver.
|
||||
*
|
||||
* @AfterStep
|
||||
* @param AfterStepScope $event
|
||||
*/
|
||||
public function takeScreenshotAfterFailedStep(AfterStepScope $event)
|
||||
{
|
||||
// Check failure code
|
||||
if ($event->getTestResult()->getResultCode() !== TestResult::FAILED) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->takeScreenshot($event);
|
||||
} catch (WebDriverException $e) {
|
||||
$this->logException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal dialog if test scenario fails on CMS page
|
||||
*
|
||||
|
@ -274,15 +308,17 @@ JS;
|
|||
*/
|
||||
public function closeModalDialog(AfterScenarioScope $event)
|
||||
{
|
||||
$expectsUnsavedChangesModal = $this->stepHasTag($event, 'unsavedChanges');
|
||||
|
||||
try {
|
||||
// Only for failed tests on CMS page
|
||||
if ($event->getTestResult()->getResultCode() === TestResult::FAILED) {
|
||||
if ($expectsUnsavedChangesModal || $event->getTestResult()->getResultCode() === TestResult::FAILED) {
|
||||
$cmsElement = $this->getSession()->getPage()->find('css', '.cms');
|
||||
if ($cmsElement) {
|
||||
try {
|
||||
// Navigate away triggered by reloading the page
|
||||
$this->getSession()->reload();
|
||||
$this->getWebDriverSession()->accept_alert();
|
||||
$this->getExpectedAlert()->accept();
|
||||
} catch (WebDriverException $e) {
|
||||
// no-op, alert might not be present
|
||||
}
|
||||
|
@ -307,65 +343,17 @@ JS;
|
|||
Filesystem::removeFolder(ASSETS_PATH, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a nice screenshot
|
||||
*
|
||||
* @param StepScope $event
|
||||
*/
|
||||
public function takeScreenshot(StepScope $event)
|
||||
{
|
||||
// Validate driver
|
||||
$driver = $this->getSession()->getDriver();
|
||||
if (!($driver instanceof Selenium2Driver)) {
|
||||
file_put_contents('php://stdout', 'ScreenShots are only supported for Selenium2Driver: skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
$feature = $event->getFeature();
|
||||
$step = $event->getStep();
|
||||
$screenshotPath = null;
|
||||
|
||||
// Check paths are configured
|
||||
$path = $this->getMainContext()->getScreenshotPath();
|
||||
if (!$path) {
|
||||
file_put_contents('php://stdout', 'ScreenShots path not configured: skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
Filesystem::makeFolder($path);
|
||||
$path = realpath($path);
|
||||
|
||||
if (!file_exists($path)) {
|
||||
file_put_contents('php://stderr', sprintf('"%s" is not valid directory and failed to create it' . PHP_EOL, $path));
|
||||
return;
|
||||
}
|
||||
|
||||
if (file_exists($path) && !is_dir($path)) {
|
||||
file_put_contents('php://stderr', sprintf('"%s" is not valid directory' . PHP_EOL, $path));
|
||||
return;
|
||||
}
|
||||
if (file_exists($path) && !is_writable($path)) {
|
||||
file_put_contents('php://stderr', sprintf('"%s" directory is not writable' . PHP_EOL, $path));
|
||||
return;
|
||||
}
|
||||
|
||||
$path = sprintf('%s/%s_%d.png', $path, basename($feature->getFile()), $step->getLine());
|
||||
$screenshot = $driver->getWebDriverSession()->screenshot();
|
||||
file_put_contents($path, base64_decode($screenshot));
|
||||
file_put_contents('php://stderr', sprintf('Saving screenshot into %s' . PHP_EOL, $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^the page can't be found/
|
||||
*/
|
||||
public function stepPageCantBeFound()
|
||||
{
|
||||
$page = $this->getSession()->getPage();
|
||||
assertTrue(
|
||||
Assert::assertTrue(
|
||||
// Content from ErrorPage default record
|
||||
$page->hasContent('Page not found')
|
||||
// Generic ModelAsController message
|
||||
|| $page->hasContent('The requested page could not be found')
|
||||
// Generic ModelAsController message
|
||||
|| $page->hasContent('The requested page could not be found')
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -376,7 +364,7 @@ JS;
|
|||
*/
|
||||
public function stepIWaitFor($secs)
|
||||
{
|
||||
$this->getSession()->wait((float)$secs*1000);
|
||||
$this->getSession()->wait((float)$secs * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -390,7 +378,7 @@ JS;
|
|||
{
|
||||
$page = $this->getSession()->getPage();
|
||||
// See https://mathiasbynens.be/notes/css-escapes
|
||||
$escapedTitle = addcslashes($title, '!"#$%&\'()*+,-./:;<=>?@[\]^`{|}~');
|
||||
$escapedTitle = addcslashes($title ?? '', '!"#$%&\'()*+,-./:;<=>?@[\]^`{|}~');
|
||||
$matchedEl = null;
|
||||
$searches = [
|
||||
['named', ['link_or_button', "'{$title}'"]],
|
||||
|
@ -419,10 +407,10 @@ JS;
|
|||
public function iShouldSeeAButton($negative, $text)
|
||||
{
|
||||
$button = $this->findNamedButton($text);
|
||||
if (trim($negative)) {
|
||||
assertNull($button, sprintf('%s button found', $text));
|
||||
if (trim($negative ?? '')) {
|
||||
Assert::assertNull($button, sprintf('%s button found', $text));
|
||||
} else {
|
||||
assertNotNull($button, sprintf('%s button not found', $text));
|
||||
Assert::assertNotNull($button, sprintf('%s button not found', $text));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -433,7 +421,25 @@ JS;
|
|||
public function stepIPressTheButton($text)
|
||||
{
|
||||
$button = $this->findNamedButton($text);
|
||||
assertNotNull($button, "{$text} button not found");
|
||||
Assert::assertNotNull($button, "{$text} button not found");
|
||||
$button->click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^I press the "([^"]*)" buttons$/
|
||||
* @param string $text A list of button names can be provided by seperating the entries with the | character.
|
||||
*/
|
||||
public function stepIPressTheButtons($text)
|
||||
{
|
||||
$buttonNames = explode('|', $text ?? '');
|
||||
foreach ($buttonNames as $name) {
|
||||
$button = $this->findNamedButton(trim($name ?? ''));
|
||||
if ($button) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert::assertNotNull($button, "{$text} button not found");
|
||||
$button->click();
|
||||
}
|
||||
|
||||
|
@ -464,6 +470,30 @@ JS;
|
|||
$this->iDismissTheDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^I click on the "([^"]+)" element$/
|
||||
* @param string $selector
|
||||
*/
|
||||
public function iClickOnTheElement($selector)
|
||||
{
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$element = $page->find('css', $selector);
|
||||
Assert::assertNotNull($element, sprintf('Element %s not found', $selector));
|
||||
$element->click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
|
||||
*
|
||||
* @When /^I click on the "([^"]+)" element, confirming the dialog$/
|
||||
* @param $selector
|
||||
*/
|
||||
public function iClickOnTheElementConfirmingTheDialog($selector)
|
||||
{
|
||||
$this->iClickOnTheElement($selector);
|
||||
$this->iConfirmTheDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element$/
|
||||
* @param string $clickType
|
||||
|
@ -478,9 +508,9 @@ JS;
|
|||
);
|
||||
$page = $this->getSession()->getPage();
|
||||
$parentElement = $page->find('css', $selector);
|
||||
assertNotNull($parentElement, sprintf('"%s" element not found', $selector));
|
||||
Assert::assertNotNull($parentElement, sprintf('"%s" element not found', $selector));
|
||||
$element = $parentElement->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text));
|
||||
assertNotNull($element, sprintf('"%s" not found', $text));
|
||||
Assert::assertNotNull($element, sprintf('"%s" not found', $text));
|
||||
$clickTypeFn = $clickTypeMap[$clickType];
|
||||
$element->$clickTypeFn();
|
||||
}
|
||||
|
@ -515,17 +545,25 @@ JS;
|
|||
$this->iDismissTheDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^the "([^"]+)" element "([^"]+)" attribute should be "([^"]*)"$/
|
||||
*/
|
||||
public function theElementAttributeShouldBe($selector, $attribute, $value)
|
||||
{
|
||||
$page = $this->getSession()->getPage();
|
||||
$element = $page->find('css', $selector);
|
||||
Assert::assertNotNull($element, sprintf('Element %s not found', $selector));
|
||||
Assert::assertEquals($value, $element->getAttribute($attribute));
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^I see the text "([^"]+)" in the alert$/
|
||||
* @param string $expected
|
||||
*/
|
||||
public function iSeeTheDialogText($expected)
|
||||
{
|
||||
$session = $this->getSession();
|
||||
/** @var Selenium2Driver $driver */
|
||||
$driver = $session->getDriver();
|
||||
$text = $driver->getWebDriverSession()->getAlert_text();
|
||||
assertContains($expected, $text);
|
||||
$text = $this->getExpectedAlert()->getText();
|
||||
Assert::assertStringContainsString($expected, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -534,7 +572,24 @@ JS;
|
|||
*/
|
||||
public function iTypeIntoTheDialog($data)
|
||||
{
|
||||
$this->getWebDriverSession()->postAlert_text([ 'text' => $data ]);
|
||||
$this->getExpectedAlert()
|
||||
->sendKeys($data)
|
||||
->accept();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for alert to appear, and return handle
|
||||
*
|
||||
* @return WebDriverAlert
|
||||
*/
|
||||
protected function getExpectedAlert()
|
||||
{
|
||||
$session = $this->getWebDriverSession();
|
||||
$session->wait()->until(
|
||||
WebDriverExpectedCondition::alertIsPresent(),
|
||||
"Alert is expected"
|
||||
);
|
||||
return $session->switchTo()->alert();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -542,7 +597,12 @@ JS;
|
|||
*/
|
||||
public function iConfirmTheDialog()
|
||||
{
|
||||
$this->getWebDriverSession()->accept_alert();
|
||||
$session = $this->getWebDriverSession();
|
||||
$session->wait()->until(
|
||||
WebDriverExpectedCondition::alertIsPresent(),
|
||||
"Alert is expected"
|
||||
);
|
||||
$session->switchTo()->alert()->accept();
|
||||
$this->handleAjaxTimeout();
|
||||
}
|
||||
|
||||
|
@ -551,23 +611,23 @@ JS;
|
|||
*/
|
||||
public function iDismissTheDialog()
|
||||
{
|
||||
$this->getWebDriverSession()->dismiss_alert();
|
||||
$this->getExpectedAlert()->dismiss();
|
||||
$this->handleAjaxTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Selenium webdriver session.
|
||||
* Note: Will fail if current driver isn't Selenium2Driver
|
||||
* Note: Will fail if current driver isn't FacebookWebDriver
|
||||
*
|
||||
* @return WebDriverSession
|
||||
* @return WebDriver
|
||||
*/
|
||||
protected function getWebDriverSession()
|
||||
{
|
||||
$driver = $this->getSession()->getDriver();
|
||||
if (! $driver instanceof Selenium2Driver) {
|
||||
throw new \InvalidArgumentException("Not supported for non-selenium2 drivers");
|
||||
if (!$driver instanceof FacebookWebDriver) {
|
||||
throw new InvalidArgumentException("Only supported for FacebookWebDriver");
|
||||
}
|
||||
return $driver->getWebDriverSession();
|
||||
return $driver->getWebDriver();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -575,9 +635,12 @@ JS;
|
|||
* @param string $field
|
||||
* @param string $path
|
||||
* @return Call\Given
|
||||
*
|
||||
* @deprecated 4.5.0 Use iAttachTheFileToTheField() instead
|
||||
*/
|
||||
public function iAttachTheFileTo($field, $path)
|
||||
{
|
||||
Deprecation::notice('4.5.0', 'Use iAttachTheFileToTheField() instead');
|
||||
// Remove wrapped button styling to make input field accessible to Selenium
|
||||
$js = <<<JS
|
||||
let input = jQuery('[name="$field"]');
|
||||
|
@ -612,7 +675,7 @@ JS;
|
|||
}
|
||||
|
||||
if (!$parent) {
|
||||
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 */
|
||||
|
@ -632,7 +695,7 @@ JS;
|
|||
}
|
||||
|
||||
if (!$input) {
|
||||
throw new \InvalidArgumentException(sprintf('Input "%s" cannot be found', $value));
|
||||
throw new InvalidArgumentException(sprintf('Input "%s" cannot be found', $value));
|
||||
}
|
||||
|
||||
$this->getSession()->getDriver()->click($input->getXPath());
|
||||
|
@ -649,6 +712,7 @@ JS;
|
|||
{
|
||||
fwrite(STDOUT, "\033[s \033[93m[Breakpoint] Press \033[1;93m[RETURN]\033[0;93m to continue...\033[0m");
|
||||
while (fgets(STDIN, 1024) == '') {
|
||||
// noop
|
||||
}
|
||||
fwrite(STDOUT, "\033[u");
|
||||
|
||||
|
@ -667,14 +731,14 @@ JS;
|
|||
*/
|
||||
public function castRelativeToAbsoluteTime($prefix, $val)
|
||||
{
|
||||
$timestamp = strtotime($val);
|
||||
$timestamp = strtotime($val ?? '');
|
||||
if (!$timestamp) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
"Can't resolve '%s' into a valid datetime value",
|
||||
$val
|
||||
));
|
||||
}
|
||||
return date($this->timeFormat, $timestamp);
|
||||
return date($this->timeFormat ?? '', $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -689,14 +753,14 @@ JS;
|
|||
*/
|
||||
public function castRelativeToAbsoluteDatetime($prefix, $val)
|
||||
{
|
||||
$timestamp = strtotime($val);
|
||||
$timestamp = strtotime($val ?? '');
|
||||
if (!$timestamp) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
"Can't resolve '%s' into a valid datetime value",
|
||||
$val
|
||||
));
|
||||
}
|
||||
return date($this->datetimeFormat, $timestamp);
|
||||
return date($this->datetimeFormat ?? '', $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -711,14 +775,14 @@ JS;
|
|||
*/
|
||||
public function castRelativeToAbsoluteDate($prefix, $val)
|
||||
{
|
||||
$timestamp = strtotime($val);
|
||||
$timestamp = strtotime($val ?? '');
|
||||
if (!$timestamp) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
"Can't resolve '%s' into a valid datetime value",
|
||||
$val
|
||||
));
|
||||
}
|
||||
return date($this->dateFormat, $timestamp);
|
||||
return date($this->dateFormat ?? '', $timestamp);
|
||||
}
|
||||
|
||||
public function getDateFormat()
|
||||
|
@ -774,13 +838,13 @@ JS;
|
|||
));
|
||||
}
|
||||
|
||||
assertNotNull($element, sprintf("Element '%s' not found", $name));
|
||||
Assert::assertNotNull($element, sprintf("Element '%s' not found", $name));
|
||||
|
||||
$disabledAttribute = $element->getAttribute('disabled');
|
||||
if (trim($negate)) {
|
||||
assertNull($disabledAttribute, sprintf("Failed asserting element '%s' is not disabled", $name));
|
||||
if (trim($negate ?? '')) {
|
||||
Assert::assertNull($disabledAttribute, sprintf("Failed asserting element '%s' is not disabled", $name));
|
||||
} else {
|
||||
assertNotNull($disabledAttribute, sprintf("Failed asserting element '%s' is disabled", $name));
|
||||
Assert::assertNotNull($disabledAttribute, sprintf("Failed asserting element '%s' is disabled", $name));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -797,11 +861,11 @@ JS;
|
|||
{
|
||||
$page = $this->getSession()->getPage();
|
||||
$fieldElement = $page->findField($field);
|
||||
assertNotNull($fieldElement, sprintf("Field '%s' not found", $field));
|
||||
Assert::assertNotNull($fieldElement, sprintf("Field '%s' not found", $field));
|
||||
|
||||
$disabledAttribute = $fieldElement->getAttribute('disabled');
|
||||
|
||||
assertNull($disabledAttribute, sprintf("Failed asserting field '%s' is enabled", $field));
|
||||
Assert::assertNull($disabledAttribute, sprintf("Failed asserting field '%s' is enabled", $field));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -820,11 +884,11 @@ JS;
|
|||
{
|
||||
$context = $this->getMainContext();
|
||||
$regionObj = $context->getRegionObj($region);
|
||||
assertNotNull($regionObj);
|
||||
Assert::assertNotNull($regionObj);
|
||||
|
||||
$linkObj = $regionObj->findLink($link);
|
||||
if (empty($linkObj)) {
|
||||
throw new \Exception(sprintf('The link "%s" was not found in the region "%s"
|
||||
throw new \Exception(sprintf('The link "%s" was not found in the region "%s"
|
||||
on the page %s', $link, $region, $this->getSession()->getCurrentUrl()));
|
||||
}
|
||||
|
||||
|
@ -846,18 +910,17 @@ JS;
|
|||
{
|
||||
$context = $this->getMainContext();
|
||||
$regionObj = $context->getRegionObj($region);
|
||||
assertNotNull($regionObj, "Region Object is null");
|
||||
Assert::assertNotNull($regionObj, "Region Object is null");
|
||||
|
||||
$fieldObj = $regionObj->findField($field);
|
||||
if (empty($fieldObj)) {
|
||||
throw new \Exception(sprintf('The field "%s" was not found in the region "%s"
|
||||
throw new \Exception(sprintf('The field "%s" was not found in the region "%s"
|
||||
on the page %s', $field, $region, $this->getSession()->getCurrentUrl()));
|
||||
}
|
||||
|
||||
$regionObj->fillField($field, $value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asserts text in a specific region (an element identified by a CSS selector, a "data-title" attribute,
|
||||
* or a named region mapped to a CSS selector via Behat configuration).
|
||||
|
@ -876,14 +939,14 @@ JS;
|
|||
{
|
||||
$context = $this->getMainContext();
|
||||
$regionObj = $context->getRegionObj($region);
|
||||
assertNotNull($regionObj);
|
||||
Assert::assertNotNull($regionObj);
|
||||
|
||||
$actual = $regionObj->getText();
|
||||
$actual = preg_replace('/\s+/u', ' ', $actual);
|
||||
$regex = '/'.preg_quote($text, '/').'/ui';
|
||||
$actual = preg_replace('/\s+/u', ' ', $actual ?? '');
|
||||
$regex = '/' . preg_quote($text ?? '', '/') . '/ui';
|
||||
|
||||
if (trim($negate)) {
|
||||
if (preg_match($regex, $actual)) {
|
||||
if (trim($negate ?? '')) {
|
||||
if (preg_match($regex ?? '', $actual ?? '')) {
|
||||
$message = sprintf(
|
||||
'The text "%s" was found in the text of the "%s" region on the page %s.',
|
||||
$text,
|
||||
|
@ -894,7 +957,7 @@ JS;
|
|||
throw new \Exception($message);
|
||||
}
|
||||
} else {
|
||||
if (!preg_match($regex, $actual)) {
|
||||
if (!preg_match($regex ?? '', $actual ?? '')) {
|
||||
$message = sprintf(
|
||||
'The text "%s" was not found anywhere in the text of the "%s" region on the page %s.',
|
||||
$text,
|
||||
|
@ -920,7 +983,7 @@ JS;
|
|||
'radio',
|
||||
$this->getMainContext()->getXpathEscaper()->escapeLiteral($radioLabel)
|
||||
]);
|
||||
assertNotNull($radioButton);
|
||||
Assert::assertNotNull($radioButton);
|
||||
$session->getDriver()->click($radioButton->getXPath());
|
||||
}
|
||||
|
||||
|
@ -934,7 +997,7 @@ JS;
|
|||
$table = $this->getTable($selector);
|
||||
|
||||
$element = $table->find('named', array('content', "'$text'"));
|
||||
assertNotNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $selector));
|
||||
Assert::assertNotNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $selector));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -947,7 +1010,7 @@ JS;
|
|||
$table = $this->getTable($selector);
|
||||
|
||||
$element = $table->find('named', array('content', "'$text'"));
|
||||
assertNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $selector));
|
||||
Assert::assertNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $selector));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -960,7 +1023,7 @@ JS;
|
|||
$table = $this->getTable($selector);
|
||||
|
||||
$element = $table->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text));
|
||||
assertNotNull($element, sprintf('Element containing `%s` not found', $text));
|
||||
Assert::assertNotNull($element, sprintf('Element containing `%s` not found', $text));
|
||||
$element->click();
|
||||
}
|
||||
|
||||
|
@ -988,17 +1051,17 @@ JS;
|
|||
);
|
||||
|
||||
// Find tables by a <caption> field
|
||||
$candidates += $page->findAll('xpath', "//table//caption[contains(normalize-space(string(.)),
|
||||
$candidates += $page->findAll('xpath', "//table//caption[contains(normalize-space(string(.)),
|
||||
$selector)]/ancestor-or-self::table[1]");
|
||||
|
||||
// Find tables by a .title node
|
||||
$candidates += $page->findAll('xpath', "//table//*[contains(concat(' ',normalize-space(@class),' '), ' title ') and contains(normalize-space(string(.)),
|
||||
$candidates += $page->findAll('xpath', "//table//*[contains(concat(' ',normalize-space(@class),' '), ' title ') and contains(normalize-space(string(.)),
|
||||
$selector)]/ancestor-or-self::table[1]");
|
||||
|
||||
// Some tables don't have a visible title, so look for a fieldset with data-name instead
|
||||
$candidates += $page->findAll('xpath', "//fieldset[@data-name=$selector]//table");
|
||||
|
||||
assertTrue((bool)$candidates, 'Could not find any table elements');
|
||||
Assert::assertTrue((bool)$candidates, 'Could not find any table elements');
|
||||
|
||||
$table = null;
|
||||
/** @var NodeElement $candidate */
|
||||
|
@ -1008,7 +1071,7 @@ JS;
|
|||
}
|
||||
}
|
||||
|
||||
assertTrue((bool)$table, 'Found table elements, but none are visible');
|
||||
Assert::assertTrue((bool)$table, 'Found table elements, but none are visible');
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
@ -1016,7 +1079,7 @@ JS;
|
|||
/**
|
||||
* Checks the order of two texts.
|
||||
* Assumptions: the two texts appear in their conjunct parent element once
|
||||
* @Then /^I should see the text "(?P<textBefore>(?:[^"]|\\")*)" (before|after) the text "(?P<textAfter>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
|
||||
* @Then /^I should see the text "(?P<textBefore>(?:[^"]|\\")*)" (?P<order>(before|after)) the text "(?P<textAfter>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
|
||||
* @param string $textBefore
|
||||
* @param string $order
|
||||
* @param string $textAfter
|
||||
|
@ -1025,19 +1088,19 @@ JS;
|
|||
public function theTextBeforeAfter($textBefore, $order, $textAfter, $element)
|
||||
{
|
||||
$ele = $this->getSession()->getPage()->find('css', $element);
|
||||
assertNotNull($ele, sprintf('%s not found', $element));
|
||||
Assert::assertNotNull($ele, sprintf('%s not found', $element));
|
||||
|
||||
// Check both of the texts exist in the element
|
||||
$text = $ele->getText();
|
||||
assertTrue(strpos($text, $textBefore) !== 'FALSE', sprintf('%s not found in the element %s', $textBefore, $element));
|
||||
assertTrue(strpos($text, $textAfter) !== 'FALSE', sprintf('%s not found in the element %s', $textAfter, $element));
|
||||
Assert::assertTrue(strpos($text ?? '', $textBefore ?? '') !== 'FALSE', sprintf('%s not found in the element %s', $textBefore, $element));
|
||||
Assert::assertTrue(strpos($text ?? '', $textAfter ?? '') !== 'FALSE', sprintf('%s not found in the element %s', $textAfter, $element));
|
||||
|
||||
/// Use strpos to get the position of the first occurrence of the two texts (case-sensitive)
|
||||
// and compare them with the given order (before or after)
|
||||
if ($order === 'before') {
|
||||
assertTrue(strpos($text, $textBefore) < strpos($text, $textAfter));
|
||||
Assert::assertTrue(strpos($text ?? '', $textBefore ?? '') < strpos($text ?? '', $textAfter ?? ''));
|
||||
} else {
|
||||
assertTrue(strpos($text, $textBefore) > strpos($text, $textAfter));
|
||||
Assert::assertTrue(strpos($text ?? '', $textBefore ?? '') > strpos($text ?? '', $textAfter ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1101,16 +1164,20 @@ JS;
|
|||
$page = $this->getSession()->getPage();
|
||||
$session = $this->getSession();
|
||||
$this->spin(function () use ($page, $session, $text) {
|
||||
$element = $page->find(
|
||||
$elements = $page->findAll(
|
||||
'xpath',
|
||||
$session->getSelectorsHandler()->selectorToXpath("xpath", ".//*[contains(text(), '$text')]")
|
||||
);
|
||||
|
||||
if (empty($element)) {
|
||||
return false;
|
||||
} else {
|
||||
return ($element->isVisible());
|
||||
foreach ($elements as $element) {
|
||||
if (empty($element)) {
|
||||
continue;
|
||||
}
|
||||
if (!$element->isVisible()) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1146,11 +1213,11 @@ JS;
|
|||
{
|
||||
$page = $this->getSession()->getPage();
|
||||
$el = $page->find('named', array($type, "'$locator'"));
|
||||
assertNotNull($el, sprintf('%s element not found', $locator));
|
||||
Assert::assertNotNull($el, sprintf('%s element not found', $locator));
|
||||
|
||||
$id = $el->getAttribute('id');
|
||||
if (empty($id)) {
|
||||
throw new \InvalidArgumentException('Element requires an "id" attribute');
|
||||
throw new InvalidArgumentException('Element requires an "id" attribute');
|
||||
}
|
||||
|
||||
$js = sprintf("document.getElementById('%s').scrollIntoView(true);", $id);
|
||||
|
@ -1169,11 +1236,11 @@ JS;
|
|||
public function iScrollToElement($locator)
|
||||
{
|
||||
$el = $this->getSession()->getPage()->find('css', $locator);
|
||||
assertNotNull($el, sprintf('The element "%s" is not found', $locator));
|
||||
Assert::assertNotNull($el, sprintf('The element "%s" is not found', $locator));
|
||||
|
||||
$id = $el->getAttribute('id');
|
||||
if (empty($id)) {
|
||||
throw new \InvalidArgumentException('Element requires an "id" attribute');
|
||||
throw new InvalidArgumentException('Element requires an "id" attribute');
|
||||
}
|
||||
|
||||
$js = sprintf("document.getElementById('%s').scrollIntoView(true);", $id);
|
||||
|
@ -1214,6 +1281,15 @@ JS;
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log a message
|
||||
*/
|
||||
protected function logMessage(string $message)
|
||||
{
|
||||
file_put_contents('php://stderr', $message . PHP_EOL);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* We have to catch exceptions and log somehow else otherwise behat falls over
|
||||
*
|
||||
|
@ -1221,6 +1297,256 @@ JS;
|
|||
*/
|
||||
protected function logException(Exception $exception)
|
||||
{
|
||||
file_put_contents('php://stderr', 'Exception caught: ' . $exception->getMessage());
|
||||
$this->logMessage('Exception caught: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect element with javascript, rather than having the selector converted to xpath
|
||||
* There's already an xpath based function 'I see the "" element' iSeeTheElement() in silverstripe/cms
|
||||
* There's also an 'I should see "" element' in MinkContext which also converts the css selector to xpath
|
||||
*
|
||||
* @When /^I should(| not) see the "([^"]+)" element/
|
||||
* @param $selector
|
||||
*/
|
||||
public function iShouldSeeTheElement($not, $cssSelector = '')
|
||||
{
|
||||
// backwards compatibility for when function signature was just ($cssSelector)
|
||||
if (!in_array($not, ['', ' not'])) {
|
||||
$not = '';
|
||||
$cssSelector = $not;
|
||||
}
|
||||
$sel = str_replace('"', '\\"', $cssSelector ?? '');
|
||||
$js = <<<JS
|
||||
return document.querySelector("$sel");
|
||||
JS;
|
||||
$element = $this->getSession()->evaluateScript($js);
|
||||
if ($not) {
|
||||
Assert::assertNull($element, sprintf('Element %s was found when it should not have been', $cssSelector));
|
||||
} else {
|
||||
Assert::assertNotNull($element, sprintf('Element %s not found', $cssSelector));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the option in select field with specified id|name|label|value
|
||||
* Also accepts CSS selectors
|
||||
*
|
||||
* @When /^I select "([^"]+)" from the "([^"]+)" field(| with javascript)$/
|
||||
* @param string $value
|
||||
* @param string $locator - select id, name, label or element
|
||||
* @param string $withJavascript - use javascript if having trouble selecting an option e.g. visibility
|
||||
*/
|
||||
public function iSelectFromTheField($value, $locator, $withJavascript)
|
||||
{
|
||||
$field = $this->getElement($locator);
|
||||
if (!$withJavascript) {
|
||||
$field->selectOption($value);
|
||||
} else {
|
||||
$xpath = $field->getXpath();
|
||||
$xpath = str_replace(['"', "\n"], ['\"', ''], $xpath ?? '');
|
||||
$value = str_replace('"', '\"', $value ?? '');
|
||||
$js = <<<JS
|
||||
return (function() {
|
||||
let select = document.evaluate("{$xpath}", document).iterateNext();
|
||||
let options = select.getElementsByTagName('option');
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
let option = options[i];
|
||||
if (option.value != "{$value}" && option.innerHTML.trim() != "{$value}") {
|
||||
continue;
|
||||
}
|
||||
select.value = option.value;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})();
|
||||
JS;
|
||||
$result = $this->getSession()->evaluateScript($js);
|
||||
Assert::assertEquals(1, $result, "Unable to select value {$value} from {$locator} with javascript");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^the rendered HTML should(| not) contain "(.+)"$/
|
||||
* @param string $not
|
||||
* @param string $htmlFragment
|
||||
*/
|
||||
public function theRenderedHtmlShouldContain($not, $htmlFragment)
|
||||
{
|
||||
$html = $this->getSession()->getPage()->getOuterHtml();
|
||||
$htmlFragment = str_replace('\"', '"', $htmlFragment ?? '');
|
||||
$contains = strpos($html ?? '', $htmlFragment ?? '') !== false;
|
||||
if ($not) {
|
||||
Assert::assertFalse($contains, "HTML fragment {$htmlFragment} was in rendered HTML when it should not have been");
|
||||
} else {
|
||||
Assert::assertTrue($contains, "HTML fragment {$htmlFragment} not found in rendered HTML");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tag values to the react TagField component which uses react-select
|
||||
*
|
||||
* @Then /^I add "([^"]+)" to the "([^"]+)" tag field$/
|
||||
* @param string $value
|
||||
* @param string $locator
|
||||
*/
|
||||
public function iAddToTheTagField($value, $locator)
|
||||
{
|
||||
$tagFieldInput = $this->getElement($locator);
|
||||
$tagFieldInput->setValue($value);
|
||||
$tagFieldInput->getParent()->getParent()->getParent()->getParent()->find('css', '.Select-menu-outer')->click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^the "([^"]+)" field should have the value "([^"]+)"$/
|
||||
* @param string $locator
|
||||
* @param string $value
|
||||
*/
|
||||
public function theFieldShouldHaveTheValue($locator, $value)
|
||||
{
|
||||
Assert::assertEquals($value, $this->getElement($locator)->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Will first attempt to find a field based on $locator
|
||||
* Will fall back to finding an element based on css selector
|
||||
*
|
||||
* @param string $locator
|
||||
* @return null|NodeElement
|
||||
*/
|
||||
private function getElement($locator): ?NodeElement
|
||||
{
|
||||
$page = $this->getSession()->getPage();
|
||||
try {
|
||||
$element = $page->findField($locator);
|
||||
} catch (ElementNotFoundException $e) {
|
||||
// noop
|
||||
}
|
||||
if (!$element) {
|
||||
$element = $page->find('css', $locator);
|
||||
}
|
||||
Assert::assertNotNull($element, "Field {$locator} was not found");
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^I drag the "([^"]+)" element to the "([^"]+)" element$/
|
||||
* @param string $locatorA
|
||||
* @param string $locatorB
|
||||
*/
|
||||
public function iDragTheElementToTheElement($locatorA, $locatorB)
|
||||
{
|
||||
$elementA = $this->getElement($locatorA);
|
||||
$elementB = $this->getElement($locatorB);
|
||||
$elementA->dragTo($elementB);
|
||||
}
|
||||
|
||||
/**
|
||||
* This doesn't seem to work quite right in practice
|
||||
* iDragTheElementToTheElement is much more reliable
|
||||
*
|
||||
* @When /^I drag the "([^"]+)" element by "(\-?[0-9]+),(\-?[0-9]+)"$/
|
||||
* @param string $locatorA
|
||||
* @param string $xOffset
|
||||
* @param string $yOffset
|
||||
*/
|
||||
public function iDragTheElementBy($locatorA, $xOffset, $yOffset)
|
||||
{
|
||||
/** @var FacebookWebDrvier $driver */
|
||||
$driver = $this->getSession()->getDriver();
|
||||
if (!($driver instanceof FacebookWebDriver)) {
|
||||
$this->logMessage('Drag and drop by offset is only supported for FacebookWebDriver: skipping');
|
||||
return;
|
||||
}
|
||||
$elementA = $this->getElement($locatorA);
|
||||
$driver->dragBy($elementA->getXpath(), (int) $xOffset, (int) $yOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Globally press the key i.e. not type into an input
|
||||
*
|
||||
* @When /^I press the "([^"]+)" key globally$/
|
||||
* @param string $keyCombo - e.g. tab / shift-tab / ctrl-c / alt-f4
|
||||
*/
|
||||
public function iPressTheKeyGlobally($keyCombo)
|
||||
{
|
||||
/** @var FacebookWebDrvier $driver */
|
||||
$driver = $this->getSession()->getDriver();
|
||||
if (!($driver instanceof FacebookWebDriver)) {
|
||||
$this->logMessage('Pressing keys globally is only supported for FacebookWebDriver: skipping');
|
||||
return;
|
||||
}
|
||||
$modifier = null;
|
||||
$pos = strpos($keyCombo ?? '', '-');
|
||||
if ($pos !== false && $pos !== 0) {
|
||||
list($modifier, $char) = explode('-', $keyCombo ?? '');
|
||||
} else {
|
||||
$char = $keyCombo;
|
||||
}
|
||||
// handle special chars e.g. "space"
|
||||
if (defined(WebDriverKeys::class . '::' . strtoupper($char ?? ''))) {
|
||||
$char = constant(WebDriverKeys::class . '::' . strtoupper($char ?? ''));
|
||||
}
|
||||
if ($modifier) {
|
||||
$modifier = strtoupper($modifier ?? '');
|
||||
if (defined(WebDriverKeys::class . '::' . $modifier)) {
|
||||
$modifier = constant(WebDriverKeys::class . '::' . $modifier);
|
||||
} else {
|
||||
$modifier = null;
|
||||
}
|
||||
}
|
||||
$driver->globalKeyPress($char, $modifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use upload fields
|
||||
*
|
||||
* @Then /^I attach the file "([^"]+)" to the "([^"]+)" field$/
|
||||
* @param $filename
|
||||
* @param $locator
|
||||
*/
|
||||
public function iAttachTheFileToTheField($filename, $locator)
|
||||
{
|
||||
Assert::assertNotNull($this->fixtureContext, 'FixtureContext was not found so cannot know location of fixture files');
|
||||
$path = $this->fixtureContext->getFilesPath() . '/' . $filename;
|
||||
$path = str_replace('//', '/', $path ?? '');
|
||||
Assert::assertNotEmpty($path, 'Fixture files path is empty');
|
||||
$field = $this->getElement($locator);
|
||||
$filesPath = $this->fixtureContext->getFilesPath();
|
||||
if ($filesPath) {
|
||||
$fullPath = rtrim(realpath($filesPath ?? '') ?? '', DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $path;
|
||||
if (is_file($fullPath ?? '')) {
|
||||
$path = $fullPath;
|
||||
}
|
||||
}
|
||||
Assert::assertFileExists($path, "{$path} does not exist");
|
||||
$field->attachFile($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to follow hyperlinks with target="_blank"
|
||||
* Behat won't switch to the new tab
|
||||
* Also allows use of css selectors
|
||||
*
|
||||
* @When /^I follow "([^"]+)" with javascript$/
|
||||
* @param string $locator
|
||||
*/
|
||||
public function iFollowWithJavascript($locator)
|
||||
{
|
||||
$page = $this->getSession()->getPage();
|
||||
$link = $page->find('named', ['link', $locator]);
|
||||
if (!$link) {
|
||||
$link = $page->find('css', $locator);
|
||||
}
|
||||
Assert::assertNotNull($link, "Link {$locator} was not found");
|
||||
$html = $link->getOuterHtml();
|
||||
preg_match('#href=([\'"])#', $html ?? '', $m);
|
||||
$q = $m[1];
|
||||
preg_match("#href={$q}(.+?){$q}#", $html ?? '', $m);
|
||||
$href = str_replace("'", "\\'", $m[1] ?? '');
|
||||
if (strpos($href ?? '', 'http') !== 0) {
|
||||
$href = rtrim($href ?? '', '/');
|
||||
$href = "/{$href}";
|
||||
}
|
||||
$this->getSession()->executeScript("document.location.href = '{$href}';");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ use Behat\Behat\Context\Context;
|
|||
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
|
||||
use Behat\Gherkin\Node\TableNode;
|
||||
use Behat\Mink\Session;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use SilverStripe\BehatExtension\Utility\TestMailer;
|
||||
use SilverStripe\Control\Email\Email;
|
||||
use SilverStripe\Control\Email\Mailer;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
@ -50,7 +50,6 @@ class EmailContext implements Context
|
|||
// to ensure its available both in CLI execution and the tested browser session
|
||||
$this->mailer = new TestMailer();
|
||||
Injector::inst()->registerService($this->mailer, Mailer::class);
|
||||
Email::config()->update("send_all_emails_to", null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,10 +63,10 @@ class EmailContext implements Context
|
|||
$to = ($direction == 'to') ? $email : null;
|
||||
$from = ($direction == 'from') ? $email : null;
|
||||
$match = $this->mailer->findEmail($to, $from);
|
||||
if (trim($negate)) {
|
||||
assertNull($match);
|
||||
if (trim($negate ?? '')) {
|
||||
Assert::assertNull($match);
|
||||
} else {
|
||||
assertNotNull($match);
|
||||
Assert::assertNotNull($match);
|
||||
}
|
||||
$this->lastMatchedEmail = $match;
|
||||
}
|
||||
|
@ -88,8 +87,8 @@ class EmailContext implements Context
|
|||
$allTitles = $allMails ? '"' . implode('","', array_map(function ($email) {
|
||||
return $email->Subject;
|
||||
}, $allMails)) . '"' : null;
|
||||
if (trim($negate)) {
|
||||
assertNull($match);
|
||||
if (trim($negate ?? '')) {
|
||||
Assert::assertNull($match);
|
||||
} else {
|
||||
$msg = sprintf(
|
||||
'Could not find email %s "%s" titled "%s".',
|
||||
|
@ -100,7 +99,7 @@ class EmailContext implements Context
|
|||
if ($allTitles) {
|
||||
$msg .= ' Existing emails: ' . $allTitles;
|
||||
}
|
||||
assertNotNull($match, $msg);
|
||||
Assert::assertNotNull($match, $msg);
|
||||
}
|
||||
$this->lastMatchedEmail = $match;
|
||||
}
|
||||
|
@ -128,10 +127,10 @@ class EmailContext implements Context
|
|||
$emailContent = $email->PlainContent;
|
||||
}
|
||||
|
||||
if (trim($negate)) {
|
||||
assertNotContains($content, $emailContent);
|
||||
if (trim($negate ?? '')) {
|
||||
Assert::assertStringNotContainsString($content, $emailContent);
|
||||
} else {
|
||||
assertContains($content, $emailContent);
|
||||
Assert::assertStringContainsString($content, $emailContent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,10 +151,10 @@ class EmailContext implements Context
|
|||
|
||||
$email = $this->lastMatchedEmail;
|
||||
$emailContent = ($email->Content) ? ($email->Content) : ($email->PlainContent);
|
||||
$emailPlainText = strip_tags($emailContent);
|
||||
$emailPlainText = preg_replace("/\h+/", " ", $emailPlainText);
|
||||
$emailPlainText = strip_tags($emailContent ?? '');
|
||||
$emailPlainText = preg_replace("/\h+/", " ", $emailPlainText ?? '');
|
||||
|
||||
assertContains($content, $emailPlainText);
|
||||
Assert::assertStringContainsString($content, $emailPlainText);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -169,13 +168,13 @@ class EmailContext implements Context
|
|||
$to = ($direction == 'to') ? $email : null;
|
||||
$from = ($direction == 'from') ? $email : null;
|
||||
$match = $this->mailer->findEmail($to, $from);
|
||||
assertNotNull($match);
|
||||
Assert::assertNotNull($match);
|
||||
|
||||
$crawler = new Crawler($match->Content);
|
||||
$linkEl = $crawler->selectLink($linkSelector);
|
||||
assertNotNull($linkEl);
|
||||
Assert::assertNotNull($linkEl);
|
||||
$link = $linkEl->attr('href');
|
||||
assertNotNull($link);
|
||||
Assert::assertNotNull($link);
|
||||
|
||||
$this->getMainContext()->visit($link);
|
||||
}
|
||||
|
@ -192,13 +191,13 @@ class EmailContext implements Context
|
|||
$to = ($direction == 'to') ? $email : null;
|
||||
$from = ($direction == 'from') ? $email : null;
|
||||
$match = $this->mailer->findEmail($to, $from, $title);
|
||||
assertNotNull($match);
|
||||
Assert::assertNotNull($match);
|
||||
|
||||
$crawler = new Crawler($match->Content);
|
||||
$linkEl = $crawler->selectLink($linkSelector);
|
||||
assertNotNull($linkEl);
|
||||
Assert::assertNotNull($linkEl);
|
||||
$link = $linkEl->attr('href');
|
||||
assertNotNull($link);
|
||||
Assert::assertNotNull($link);
|
||||
$this->getMainContext()->visit($link);
|
||||
}
|
||||
|
||||
|
@ -218,9 +217,9 @@ class EmailContext implements Context
|
|||
$match = $this->lastMatchedEmail;
|
||||
$crawler = new Crawler($match->Content);
|
||||
$linkEl = $crawler->selectLink($linkSelector);
|
||||
assertNotNull($linkEl);
|
||||
Assert::assertNotNull($linkEl);
|
||||
$link = $linkEl->attr('href');
|
||||
assertNotNull($link);
|
||||
Assert::assertNotNull($link);
|
||||
|
||||
$this->getMainContext()->visit($link);
|
||||
}
|
||||
|
@ -257,18 +256,18 @@ class EmailContext implements Context
|
|||
$emailContent = $email->PlainContent;
|
||||
}
|
||||
// Convert html content to plain text
|
||||
$emailContent = strip_tags($emailContent);
|
||||
$emailContent = preg_replace("/\h+/", " ", $emailContent);
|
||||
$emailContent = strip_tags($emailContent ?? '');
|
||||
$emailContent = preg_replace("/\h+/", " ", $emailContent ?? '');
|
||||
$rows = $table->getRows();
|
||||
|
||||
// For "should not contain"
|
||||
if (trim($negate)) {
|
||||
if (trim($negate ?? '')) {
|
||||
foreach ($rows as $row) {
|
||||
assertNotContains($row[0], $emailContent);
|
||||
Assert::assertStringNotContainsString($row[0], $emailContent);
|
||||
}
|
||||
} else {
|
||||
foreach ($rows as $row) {
|
||||
assertContains($row[0], $emailContent);
|
||||
Assert::assertStringContainsString($row[0], $emailContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -281,14 +280,14 @@ class EmailContext implements Context
|
|||
public function thereIsAnEmailTitled($negate, $subject)
|
||||
{
|
||||
$match = $this->mailer->findEmail(null, null, $subject);
|
||||
if (trim($negate)) {
|
||||
assertNull($match);
|
||||
if (trim($negate ?? '')) {
|
||||
Assert::assertNull($match);
|
||||
} else {
|
||||
$msg = sprintf(
|
||||
'Could not find email titled "%s".',
|
||||
$subject
|
||||
);
|
||||
assertNotNull($match, $msg);
|
||||
Assert::assertNotNull($match, $msg);
|
||||
}
|
||||
$this->lastMatchedEmail = $match;
|
||||
}
|
||||
|
@ -305,10 +304,10 @@ class EmailContext implements Context
|
|||
}
|
||||
|
||||
$match = $this->lastMatchedEmail;
|
||||
if (trim($negate)) {
|
||||
assertNotContains($from, $match->From);
|
||||
if (trim($negate ?? '')) {
|
||||
Assert::assertStringNotContainsString($from, $match->From);
|
||||
} else {
|
||||
assertContains($from, $match->From);
|
||||
Assert::assertStringContainsString($from, $match->From);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,10 +323,10 @@ class EmailContext implements Context
|
|||
}
|
||||
|
||||
$match = $this->lastMatchedEmail;
|
||||
if (trim($negate)) {
|
||||
assertNotContains($to, $match->To);
|
||||
if (trim($negate ?? '')) {
|
||||
Assert::assertStringNotContainsString($to, $match->To);
|
||||
} else {
|
||||
assertContains($to, $match->To);
|
||||
Assert::assertStringContainsString($to, $match->To);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -353,12 +352,12 @@ class EmailContext implements Context
|
|||
$href = null;
|
||||
foreach ($tags as $tag) {
|
||||
$linkText = $tag->nodeValue;
|
||||
if (strpos($linkText, $httpText) !== false) {
|
||||
if (strpos($linkText ?? '', $httpText ?? '') !== false) {
|
||||
$href = $linkText;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull($href);
|
||||
Assert::assertNotNull($href);
|
||||
|
||||
$this->getMainContext()->visit($href);
|
||||
}
|
||||
|
|
|
@ -9,22 +9,28 @@ use Behat\Gherkin\Node\PyStringNode;
|
|||
use Behat\Gherkin\Node\TableNode;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use SilverStripe\Assets\Folder;
|
||||
use SilverStripe\Assets\Storage\AssetStore;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Extensible;
|
||||
use SilverStripe\Core\Extension;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Core\Manifest\ModuleLoader;
|
||||
use SilverStripe\Core\Manifest\ModuleManifest;
|
||||
use SilverStripe\Dev\BehatFixtureFactory;
|
||||
use SilverStripe\Dev\FixtureBlueprint;
|
||||
use SilverStripe\Dev\FixtureFactory;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Dev\YamlFixture;
|
||||
use SilverStripe\ORM\Connect\TempDatabase;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\Security\Group;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
|
||||
/**
|
||||
* Context used to create fixtures in the SilverStripe ORM.
|
||||
|
@ -55,10 +61,15 @@ class FixtureContext implements Context
|
|||
protected $tempDatabase;
|
||||
|
||||
/**
|
||||
* @var String Tracks all files and folders created from fixtures, for later cleanup.
|
||||
* @var string[] Tracks all files and folders created from fixtures, for later cleanup.
|
||||
*/
|
||||
protected $createdFilesPaths = array();
|
||||
|
||||
/**
|
||||
* @var string[] Tracks any config files that have been activated as part of a scenario
|
||||
*/
|
||||
protected $activatedConfigFiles = array();
|
||||
|
||||
/**
|
||||
* @var array Stores the asset tuples.
|
||||
*/
|
||||
|
@ -247,12 +258,12 @@ class FixtureContext implements Context
|
|||
$class = $this->convertTypeToClass($type);
|
||||
preg_match_all(
|
||||
'/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/',
|
||||
$data,
|
||||
$data ?? '',
|
||||
$matches
|
||||
);
|
||||
$fields = $this->convertFields(
|
||||
$class,
|
||||
array_combine($matches['key'], $matches['value'])
|
||||
array_combine($matches['key'] ?? [], $matches['value'] ?? [])
|
||||
);
|
||||
$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
|
||||
|
@ -279,22 +290,23 @@ class FixtureContext implements Context
|
|||
* @param string $null
|
||||
* @param TableNode $fieldsTable
|
||||
*/
|
||||
public function stepCreateRecordWithTable($type, $id, $null, TableNode $fieldsTable)
|
||||
public function stepCreateRecordWithTable($type, $id, TableNode $fieldsTable)
|
||||
{
|
||||
|
||||
$class = $this->convertTypeToClass($type);
|
||||
// TODO Support more than one record
|
||||
$fields = $this->convertFields($class, $fieldsTable->getRowsHash());
|
||||
$fields = $this->prepareFixture($class, $id, $fields);
|
||||
|
||||
// We should check if this fixture object already exists - if it does, we update it. If not, we create it
|
||||
if ($existingFixture = $this->fixtureFactory->get($class, $id)) {
|
||||
if ($existingFixture = $this->getFixtureFactory()->get($class, $id)) {
|
||||
// Merge existing data with new data, and create new object to replace existing object
|
||||
foreach ($fields as $k => $v) {
|
||||
$existingFixture->$k = $v;
|
||||
}
|
||||
$existingFixture->write();
|
||||
} else {
|
||||
$this->fixtureFactory->createObject($class, $id, $fields);
|
||||
$this->getFixtureFactory()->createObject($class, $id, $fields);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -393,20 +405,20 @@ class FixtureContext implements Context
|
|||
$manyField = null;
|
||||
$oneField = null;
|
||||
if ($relationObj->manyMany()) {
|
||||
$manyField = array_search($class, $relationObj->manyMany());
|
||||
if ($manyField && strlen($relationName) > 0) {
|
||||
$manyField = array_search($class, $relationObj->manyMany() ?? []);
|
||||
if ($manyField && strlen($relationName ?? '') > 0) {
|
||||
$manyField = $relationName;
|
||||
}
|
||||
}
|
||||
if (empty($manyField) && $relationObj->hasMany(true)) {
|
||||
$manyField = array_search($class, $relationObj->hasMany());
|
||||
if ($manyField && strlen($relationName) > 0) {
|
||||
$manyField = array_search($class, $relationObj->hasMany() ?? []);
|
||||
if ($manyField && strlen($relationName ?? '') > 0) {
|
||||
$manyField = $relationName;
|
||||
}
|
||||
}
|
||||
if (empty($manyField) && $relationObj->hasOne()) {
|
||||
$oneField = array_search($class, $relationObj->hasOne());
|
||||
if ($oneField && strlen($relationName) > 0) {
|
||||
$oneField = array_search($class, $relationObj->hasOne() ?? []);
|
||||
if ($oneField && strlen($relationName ?? '') > 0) {
|
||||
$oneField = $relationName;
|
||||
}
|
||||
}
|
||||
|
@ -526,7 +538,10 @@ class FixtureContext implements Context
|
|||
}
|
||||
|
||||
/** @var Member $member */
|
||||
$member = $this->getFixtureFactory()->createObject(Member::class, $id);
|
||||
$member = $this->getFixtureFactory()->get(Member::class, $id);
|
||||
if (!$member) {
|
||||
$member = $this->getFixtureFactory()->createObject(Member::class, $id);
|
||||
}
|
||||
$member->Groups()->add($group);
|
||||
}
|
||||
|
||||
|
@ -542,12 +557,12 @@ class FixtureContext implements Context
|
|||
{
|
||||
preg_match_all(
|
||||
'/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/',
|
||||
$data,
|
||||
$data ?? '',
|
||||
$matches
|
||||
);
|
||||
$fields = $this->convertFields(
|
||||
Member::class,
|
||||
array_combine($matches['key'], $matches['value'])
|
||||
array_combine($matches['key'] ?? [], $matches['value'] ?? [])
|
||||
);
|
||||
|
||||
/** @var Group $group */
|
||||
|
@ -557,7 +572,10 @@ class FixtureContext implements Context
|
|||
}
|
||||
|
||||
/** @var Member $member */
|
||||
$member = $this->getFixtureFactory()->createObject(Member::class, $id, $fields);
|
||||
$member = $this->getFixtureFactory()->get(Member::class, $id);
|
||||
if (!$member) {
|
||||
$member = $this->getFixtureFactory()->createObject(Member::class, $id, $fields);
|
||||
}
|
||||
$member->Groups()->add($group);
|
||||
}
|
||||
|
||||
|
@ -571,7 +589,7 @@ class FixtureContext implements Context
|
|||
public function stepCreateGroupWithPermissions($id, $permissionStr)
|
||||
{
|
||||
// Convert natural language permissions to codes
|
||||
preg_match_all('/"([^"]+)"/', $permissionStr, $matches);
|
||||
preg_match_all('/"([^"]+)"/', $permissionStr ?? '', $matches);
|
||||
$permissions = $matches[1];
|
||||
$codes = Permission::get_codes(false);
|
||||
|
||||
|
@ -626,6 +644,75 @@ class FixtureContext implements Context
|
|||
$this->getMainContext()->getSession()->visit($this->getMainContext()->locatePath($link));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $extension
|
||||
* @param $class
|
||||
*
|
||||
* @Given I add an extension :extension to the :class class
|
||||
*/
|
||||
public function iAddAnExtensionToTheClass($extension, $class)
|
||||
{
|
||||
// Validate the extension
|
||||
Assert::assertTrue(
|
||||
class_exists($extension ?? '') && is_subclass_of($extension, Extension::class),
|
||||
'Given extension does not extend Extension'
|
||||
);
|
||||
|
||||
// Add the extension to the CLI context
|
||||
/** @var Extensible $targetClass */
|
||||
try {
|
||||
$targetClass = $this->convertTypeToClass($class);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// will end up here if the class is not a subclass of DataObject
|
||||
if (class_exists($class)) {
|
||||
$targetClass = $class;
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$targetClass::add_extension($extension);
|
||||
|
||||
// Write config for this extension too...
|
||||
$snakedExtension = strtolower(str_replace('\\', '-', $extension ?? '') ?? '');
|
||||
$config = <<<YAML
|
||||
---
|
||||
Name: testonly-enable-extension-$snakedExtension
|
||||
---
|
||||
$class:
|
||||
extensions:
|
||||
- $extension
|
||||
YAML;
|
||||
|
||||
$filename = 'enable-' . $snakedExtension . '.yml';
|
||||
$destPath = $this->getDestinationConfigFolder($filename);
|
||||
file_put_contents($destPath ?? '', $config);
|
||||
|
||||
// Remember to cleanup...
|
||||
$this->activatedConfigFiles[] = $destPath;
|
||||
|
||||
// Flush website. We'll need to dev/build too if it's a DataExtension
|
||||
if (is_subclass_of($extension, DataExtension::class)) {
|
||||
$this->getMainContext()->visit('/dev/build?flush');
|
||||
} else {
|
||||
$this->getMainContext()->visit('/?flush');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the destination folder for config and assert the given file name doesn't exist within in.
|
||||
*
|
||||
* @param $filename
|
||||
* @return string
|
||||
*/
|
||||
protected function getDestinationConfigFolder($filename)
|
||||
{
|
||||
$project = ModuleManifest::config()->get('project') ?: 'mysite';
|
||||
$mysite = ModuleLoader::getModule($project);
|
||||
Assert::assertNotNull($mysite, 'Project exists');
|
||||
$destPath = $mysite->getResource("_config/{$filename}")->getPath();
|
||||
Assert::assertFileDoesNotExist($destPath, "Config file {$filename} hasn't aleady been loaded");
|
||||
return $destPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a file or folder exists in the webroot.
|
||||
|
@ -637,7 +724,7 @@ class FixtureContext implements Context
|
|||
*/
|
||||
public function stepThereShouldBeAFileOrFolder($type, $path)
|
||||
{
|
||||
assertFileExists($this->joinPaths(BASE_PATH, $path));
|
||||
Assert::assertFileExists($this->joinPaths(BASE_PATH, $path));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -652,7 +739,7 @@ class FixtureContext implements Context
|
|||
public function stepThereShouldBeAFileWithTuple($filename, $hash)
|
||||
{
|
||||
$exists = $this->getAssetStore()->exists($filename, $hash);
|
||||
assertTrue((bool)$exists, "A file exists with filename $filename and hash $hash");
|
||||
Assert::assertTrue((bool)$exists, "A file exists with filename $filename and hash $hash");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -665,8 +752,8 @@ class FixtureContext implements Context
|
|||
*/
|
||||
public function lookupFixtureReference($string)
|
||||
{
|
||||
if (preg_match('/^=>/', $string)) {
|
||||
list($className, $identifier) = explode('.', preg_replace('/^=>/', '', $string), 2);
|
||||
if (preg_match('/^=>/', $string ?? '')) {
|
||||
list($className, $identifier) = explode('.', preg_replace('/^=>/', '', $string ?? '') ?? '', 2);
|
||||
$id = $this->getFixtureFactory()->getId($className, $identifier);
|
||||
if (!$id) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
|
@ -692,7 +779,7 @@ class FixtureContext implements Context
|
|||
$class = $this->convertTypeToClass($type);
|
||||
$fields = $this->prepareFixture($class, $id);
|
||||
$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();
|
||||
$field = ($mod == 'created') ? 'Created' : 'LastEdited';
|
||||
DB::prepared_query(
|
||||
|
@ -708,6 +795,19 @@ class FixtureContext implements Context
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all config files after scenario
|
||||
*
|
||||
* @AfterScenario
|
||||
* @param AfterScenarioScope $event
|
||||
*/
|
||||
public function afterResetConfig(AfterScenarioScope $event)
|
||||
{
|
||||
$this->clearConfigFiles();
|
||||
// Flush
|
||||
$this->getMainContext()->visit('/?flush');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a fixture for use
|
||||
*
|
||||
|
@ -732,8 +832,8 @@ class FixtureContext implements Context
|
|||
|
||||
|
||||
$relativeTargetPath = (isset($data['Filename'])) ? $data['Filename'] : $identifier;
|
||||
$relativeTargetPath = preg_replace('/^' . ASSETS_DIR . '\/?/', '', $relativeTargetPath);
|
||||
$sourcePath = $this->joinPaths($this->getFilesPath(), basename($relativeTargetPath));
|
||||
$relativeTargetPath = preg_replace('/^' . ASSETS_DIR . '\/?/', '', $relativeTargetPath ?? '');
|
||||
$sourcePath = $this->joinPaths($this->getFilesPath(), basename($relativeTargetPath ?? ''));
|
||||
|
||||
// Create file or folder on filesystem
|
||||
if ($class == 'SilverStripe\\Assets\\Folder' || is_subclass_of($class, 'SilverStripe\\Assets\\Folder')) {
|
||||
|
@ -741,7 +841,7 @@ class FixtureContext implements Context
|
|||
$data['ID'] = $parent->ID;
|
||||
} else {
|
||||
// Check file exists
|
||||
if (!file_exists($sourcePath)) {
|
||||
if (!file_exists($sourcePath ?? '')) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Source file for "%s" cannot be found in "%s"',
|
||||
$relativeTargetPath,
|
||||
|
@ -751,8 +851,8 @@ class FixtureContext implements Context
|
|||
|
||||
// Get parent
|
||||
$parentID = 0;
|
||||
if (strstr($relativeTargetPath, '/')) {
|
||||
$folderName = dirname($relativeTargetPath);
|
||||
if (strstr($relativeTargetPath ?? '', '/')) {
|
||||
$folderName = dirname($relativeTargetPath ?? '');
|
||||
$parent = Folder::find_or_make($folderName);
|
||||
if ($parent) {
|
||||
$parentID = $parent->ID;
|
||||
|
@ -776,7 +876,7 @@ class FixtureContext implements Context
|
|||
$data['FileVariant'] = $asset['Variant'];
|
||||
}
|
||||
if (!isset($data['Name'])) {
|
||||
$data['Name'] = basename($relativeTargetPath);
|
||||
$data['Name'] = basename($relativeTargetPath ?? '');
|
||||
}
|
||||
|
||||
// Save assets
|
||||
|
@ -795,6 +895,70 @@ class FixtureContext implements Context
|
|||
return Injector::inst()->get(AssetStore::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the first match of $select in the given HTML editor (tinymce)
|
||||
*/
|
||||
protected function selectInTheHtmlField(string $select, string $field)
|
||||
{
|
||||
$inputField = $this->getHtmlField($field);
|
||||
$inputField->getParent()->find('css', 'iframe')->click();
|
||||
$inputFieldId = $inputField->getAttribute('id');
|
||||
$js = <<<JS
|
||||
var editor = jQuery('#$inputFieldId').entwine('ss').getEditor(),
|
||||
doc = editor.getInstance().getDoc(),
|
||||
sel = doc.getSelection(),
|
||||
rng = new Range(),
|
||||
matched = false;
|
||||
|
||||
jQuery(doc).find("$select").each(function() {
|
||||
if(!matched) {
|
||||
rng.selectNode(this);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(rng);
|
||||
matched = true;
|
||||
}
|
||||
});
|
||||
JS;
|
||||
$this->getMainContext()->getSession()->executeScript($js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the first image match in the HTML editor (tinymce)
|
||||
*
|
||||
* @When /^I select the image "([^"]+)" in the "([^"]+)" HTML field$/
|
||||
* @param string $filename
|
||||
* @param string $field
|
||||
*/
|
||||
public function iSelectTheImageInHtmlField($filename, $field)
|
||||
{
|
||||
$this->selectInTheHtmlField("img[src*='$filename']", $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate an HTML editor field
|
||||
*
|
||||
* @param string $locator Raw html field identifier as passed from
|
||||
* @return NodeElement
|
||||
*/
|
||||
protected function getHtmlField($locator)
|
||||
{
|
||||
$locator = str_replace('\\"', '"', $locator ?? '');
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$element = $page->find('css', 'textarea.htmleditor[name=\'' . $locator . '\']');
|
||||
if ($element) {
|
||||
return $element;
|
||||
}
|
||||
$label = $page->findAll('xpath', sprintf('//label[contains(text(), \'%s\')]', $locator));
|
||||
if (!empty($label)) {
|
||||
Assert::assertCount(1, $label, "Found more than one element containing the phrase \"$locator\"");
|
||||
$label = array_shift($label);
|
||||
$fieldId = $label->getAttribute('for');
|
||||
$element = $page->find('css', '#' . $fieldId);
|
||||
}
|
||||
Assert::assertNotNull($element, sprintf('HTML field "%s" not found', $locator));
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a natural language class description to an actual class name.
|
||||
* Respects {@link DataObject::$singular_name} variations.
|
||||
|
@ -805,17 +969,17 @@ class FixtureContext implements Context
|
|||
*/
|
||||
protected function convertTypeToClass($type)
|
||||
{
|
||||
$type = trim($type);
|
||||
$type = trim($type ?? '');
|
||||
|
||||
// Try direct mapping
|
||||
$class = str_replace(' ', '', ucwords($type));
|
||||
if (class_exists($class) && is_subclass_of($class, 'SilverStripe\\ORM\\DataObject')) {
|
||||
$class = str_replace(' ', '', ucwords($type ?? ''));
|
||||
if (class_exists($class ?? '') && is_subclass_of($class, DataObject::class)) {
|
||||
return ClassInfo::class_name($class);
|
||||
}
|
||||
|
||||
// Fall back to singular names
|
||||
foreach (array_values(ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject')) as $candidate) {
|
||||
if (strcasecmp(singleton($candidate)->singular_name(), $type) === 0) {
|
||||
foreach (array_values(ClassInfo::subclassesFor(DataObject::class) ?? []) as $candidate) {
|
||||
if (class_exists($candidate ?? '') && strcasecmp(singleton($candidate)->singular_name() ?? '', $type ?? '') === 0) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
@ -838,7 +1002,7 @@ class FixtureContext implements Context
|
|||
{
|
||||
$labels = singleton($class)->fieldLabels();
|
||||
foreach ($fields as $fieldName => $fieldVal) {
|
||||
if ($fieldLabelKey = array_search($fieldName, $labels)) {
|
||||
if ($fieldLabelKey = array_search($fieldName, $labels ?? [])) {
|
||||
unset($fields[$fieldName]);
|
||||
$fields[$labels[$fieldLabelKey]] = $fieldVal;
|
||||
}
|
||||
|
@ -854,11 +1018,34 @@ class FixtureContext implements Context
|
|||
$paths = array_merge($paths, (array)$arg);
|
||||
}
|
||||
foreach ($paths as &$path) {
|
||||
$path = trim($path, '/');
|
||||
$path = trim($path ?? '', '/');
|
||||
}
|
||||
if (substr($args[0], 0, 1) == '/') {
|
||||
if (substr($args[0] ?? '', 0, 1) == '/') {
|
||||
$paths[0] = '/' . $paths[0];
|
||||
}
|
||||
return join('/', $paths);
|
||||
}
|
||||
|
||||
protected function clearConfigFiles()
|
||||
{
|
||||
// No files to cleanup
|
||||
if (empty($this->activatedConfigFiles)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->activatedConfigFiles as $configFile) {
|
||||
if (file_exists($configFile ?? '')) {
|
||||
unlink($configFile ?? '');
|
||||
}
|
||||
}
|
||||
$this->activatedConfigFiles = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch situations where failed scenarios and early exiting would prevent cleanup.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->clearConfigFiles();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,14 @@ namespace SilverStripe\BehatExtension\Context;
|
|||
|
||||
use Behat\Behat\Context\Context;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use LogicException;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use SilverStripe\Security\Authenticator;
|
||||
use SilverStripe\Security\Group;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\MFA\Model\RegisteredMethod;
|
||||
|
||||
/**
|
||||
* LoginContext
|
||||
|
@ -30,9 +33,9 @@ class LoginContext implements Context
|
|||
|
||||
$this->getMainContext()->getSession()->visit($adminUrl);
|
||||
|
||||
if (0 == strpos($this->getMainContext()->getSession()->getCurrentUrl(), $loginUrl)) {
|
||||
if (0 == strpos($this->getMainContext()->getSession()->getCurrentUrl() ?? '', $loginUrl ?? '')) {
|
||||
$this->stepILogInWith('admin', 'password');
|
||||
assertStringStartsWith($adminUrl, $this->getMainContext()->getSession()->getCurrentUrl());
|
||||
Assert::assertStringStartsWith($adminUrl, $this->getMainContext()->getSession()->getCurrentUrl());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,6 +54,26 @@ class LoginContext implements Context
|
|||
$this->stepILogInWith($email, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and login as a member of a group with the correct permissions.
|
||||
* Example: Given I am logged in as a member of "ADMIN" group
|
||||
*
|
||||
* @Given /^I am logged in as a member of "([^"]*)" group$/
|
||||
* @param string $groupName
|
||||
*/
|
||||
public function iAmLoggedInAsMemberOfGroup($groupName)
|
||||
{
|
||||
$group = Group::get()->filter('Title', "$groupName")->first();
|
||||
if (!$group) {
|
||||
throw new LogicException("Group $groupName does not exist");
|
||||
}
|
||||
|
||||
$email = "{$groupName}@example.org";
|
||||
$password = 'Secret!123';
|
||||
$this->generateMember($email, $password, $group, $groupName);
|
||||
$this->stepILogInWith($email, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^I am not logged in$/
|
||||
*/
|
||||
|
@ -63,13 +86,13 @@ class LoginContext implements Context
|
|||
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$form = $page->findById('LogoutForm_Form');
|
||||
assertNotNull($form, 'Logout form not found');
|
||||
Assert::assertNotNull($form, 'Logout form not found');
|
||||
|
||||
$submitButton = $form->find('css', '[type=submit]');
|
||||
$securityID = $form->find('css', '[name=SecurityID]');
|
||||
|
||||
assertNotNull($submitButton, 'Submit button on logout form not found');
|
||||
assertNotNull($securityID, 'CSRF token not found');
|
||||
Assert::assertNotNull($submitButton, 'Submit button on logout form not found');
|
||||
Assert::assertNotNull($securityID, 'CSRF token not found');
|
||||
|
||||
$submitButton->press();
|
||||
}
|
||||
|
@ -80,13 +103,77 @@ class LoginContext implements Context
|
|||
* @param string $password
|
||||
*/
|
||||
public function stepILogInWith($email, $password)
|
||||
{
|
||||
$this->loginWith($email, $password);
|
||||
|
||||
// Check if MFA module is installed
|
||||
if (!class_exists(RegisteredMethod::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip MFA registration if MFA module installed
|
||||
$this->getMainContext()->getSession()->wait(100);
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$mfa = $this->waitForElement('#mfa-app');
|
||||
if (!$mfa) {
|
||||
return;
|
||||
}
|
||||
$clicked = false;
|
||||
$cssLocator = '.mfa-action-list__item .btn';
|
||||
$this->waitForElement($cssLocator);
|
||||
foreach ($page->findAll('css', $cssLocator) as $btn) {
|
||||
if ($btn->getText() !== 'Setup later') {
|
||||
continue;
|
||||
}
|
||||
// There's been issues clicking the button, so try waiting for 0.3 seconds
|
||||
usleep(0.3 * 1000000);
|
||||
$btn->click();
|
||||
$clicked = true;
|
||||
break;
|
||||
}
|
||||
Assert::assertTrue($clicked, 'MFA "Setup later" button was not found so it was not clicked');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $cssLocator
|
||||
* @return NodeElement|null
|
||||
*/
|
||||
private function waitForElement($cssLocator)
|
||||
{
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$el = null;
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$el = $page->find('css', $cssLocator);
|
||||
if ($el) {
|
||||
break;
|
||||
}
|
||||
$this->getMainContext()->getSession()->wait(100);
|
||||
}
|
||||
return $el;
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^I log in with "([^"]*)" and "([^"]*)" without skipping MFA$/
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
*/
|
||||
public function stepILogInWithWithoutSkippingMfa($email, $password)
|
||||
{
|
||||
$this->loginWith($email, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
*/
|
||||
private function loginWith($email, $password)
|
||||
{
|
||||
$c = $this->getMainContext();
|
||||
$loginUrl = $c->joinUrlParts($c->getBaseUrl(), $c->getLoginUrl());
|
||||
$this->getMainContext()->getSession()->visit($loginUrl);
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$form = $page->findById('MemberLoginForm_LoginForm');
|
||||
assertNotNull($form, 'Login form not found');
|
||||
Assert::assertNotNull($form, 'Login form not found');
|
||||
|
||||
// Try to find visible forms again on login page.
|
||||
$visibleForm = null;
|
||||
|
@ -94,21 +181,32 @@ class LoginContext implements Context
|
|||
if ($form->isVisible() && $form->find('css', '[name=Email]')) {
|
||||
$visibleForm = $form;
|
||||
}
|
||||
assertNotNull($visibleForm, 'Could not find login email field');
|
||||
Assert::assertNotNull($visibleForm, 'Could not find login email field');
|
||||
|
||||
$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');
|
||||
Assert::assertNotNull($emailField, 'Email field on login form not found');
|
||||
Assert::assertNotNull($passwordField, 'Password field on login form not found');
|
||||
Assert::assertNotNull($submitButton, 'Submit button on login form not found');
|
||||
Assert::assertNotNull($securityID, 'CSRF token not found');
|
||||
|
||||
$emailField->setValue($email);
|
||||
$passwordField->setValue($password);
|
||||
$submitButton->press();
|
||||
|
||||
// Wait 100 ms
|
||||
$this->getMainContext()->getSession()->wait(100);
|
||||
|
||||
// In case of login error, throw exception
|
||||
// E.g. 'Your session has expired. Please re-submit the form.'
|
||||
// This will allow @retry
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$message = $page->find('css', '.message.error');
|
||||
$error = $message ? $message->getText() : null;
|
||||
Assert::assertNull($message, 'Could not log in with user ' . $email . '. Error: "' . $error. '""');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -118,7 +216,7 @@ class LoginContext implements Context
|
|||
{
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$loginForm = $page->find('css', '#MemberLoginForm_LoginForm');
|
||||
assertNotNull($loginForm, 'I should see a log-in form');
|
||||
Assert::assertNotNull($loginForm, 'I should see a log-in form');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -128,7 +226,7 @@ class LoginContext implements Context
|
|||
{
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$logoutForm = $page->find('css', '#LogoutForm_Form');
|
||||
assertNotNull($logoutForm, 'I should see a log-out form');
|
||||
Assert::assertNotNull($logoutForm, 'I should see a log-out form');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -139,7 +237,7 @@ class LoginContext implements Context
|
|||
{
|
||||
$page = $this->getMainContext()->getSession()->getPage();
|
||||
$message = $page->find('css', sprintf('.message.%s', $type));
|
||||
assertNotNull($message, sprintf('%s message not found.', $type));
|
||||
Assert::assertNotNull($message, sprintf('%s message not found.', $type));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -152,10 +250,10 @@ class LoginContext implements Context
|
|||
{
|
||||
/** @var Member $member */
|
||||
$member = Member::get()->filter('Email', $id)->First();
|
||||
assertNotNull($member);
|
||||
Assert::assertNotNull($member);
|
||||
$authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::CHECK_PASSWORD);
|
||||
foreach ($authenticators as $authenticator) {
|
||||
assertTrue($authenticator->checkPassword($member, $password)->isValid());
|
||||
Assert::assertTrue($authenticator->checkPassword($member, $password)->isValid());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,22 +266,48 @@ class LoginContext implements Context
|
|||
* @return Member
|
||||
*/
|
||||
protected function generateMemberWithPermission($email, $password, $permCode)
|
||||
{
|
||||
$group = $this->generateGroupWithPermission($permCode);
|
||||
|
||||
return $this->generateMember($email, $password, $group, $permCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or generate a group with the given permission code
|
||||
*
|
||||
* @param string $permCode
|
||||
* @return Member
|
||||
*/
|
||||
protected function generateGroupWithPermission($permCode)
|
||||
{
|
||||
// Get or create group
|
||||
$group = Group::get()->filter('Title', "$permCode group")->first();
|
||||
if (!$group) {
|
||||
$group = Group::create();
|
||||
$group->Title = "$permCode group";
|
||||
$group->write();
|
||||
}
|
||||
|
||||
$group->Title = "$permCode group";
|
||||
$group->write();
|
||||
|
||||
// Get or create permission
|
||||
$permission = Permission::create();
|
||||
$permission->Code = $permCode;
|
||||
$permission->write();
|
||||
$group->Permissions()->add($permission);
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or generate a member with the given permission code and permission group
|
||||
*
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
* @param object $group
|
||||
* @param string $identifier
|
||||
* @return Member
|
||||
*/
|
||||
protected function generateMember($email, $password, $group, $identifier)
|
||||
{
|
||||
// Get or create member
|
||||
$member = Member::get()->filter('Email', $email)->first();
|
||||
if (!$member) {
|
||||
|
@ -193,7 +317,7 @@ class LoginContext implements Context
|
|||
// 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->FirstName = $identifier;
|
||||
$member->Surname = "User";
|
||||
$member->Email = $email;
|
||||
$member->PasswordEncryption = "none";
|
||||
|
|
|
@ -2,17 +2,21 @@
|
|||
|
||||
namespace SilverStripe\BehatExtension\Context;
|
||||
|
||||
use Behat\Behat\Hook\Scope\AfterStepScope;
|
||||
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Behat\Mink\Exception\ElementNotFoundException;
|
||||
use Behat\Mink\Exception\UnsupportedDriverActionException;
|
||||
use Behat\Mink\Selector\Xpath\Escaper;
|
||||
use Behat\MinkExtension\Context\MinkContext;
|
||||
use Behat\Mink\Driver\Selenium2Driver;
|
||||
use Behat\Mink\Exception\UnsupportedDriverActionException;
|
||||
use Behat\Mink\Exception\ElementNotFoundException;
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\BehatExtension\Utility\TestMailer;
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Core\Environment;
|
||||
use SilverStripe\Core\Resettable;
|
||||
use SilverStripe\MinkFacebookWebDriver\FacebookWebDriver;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\TestSession\TestSessionEnvironment;
|
||||
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
|
||||
|
@ -83,7 +87,7 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
*/
|
||||
public function __construct(array $parameters = null)
|
||||
{
|
||||
if (!preg_match('/\\FeatureContext$/', get_class($this))) {
|
||||
if (!preg_match('#[\\\]FeatureContext$#', get_class($this))) {
|
||||
throw new InvalidArgumentException(
|
||||
'Subclasses of SilverStripeContext must be named FeatureContext. Found "' . get_class($this) . '""'
|
||||
);
|
||||
|
@ -187,7 +191,7 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
$regionObj = $this->getSession()->getPage()->find(
|
||||
'css',
|
||||
// Escape CSS selector
|
||||
(false !== strpos($region, "'")) ? str_replace("'", "\\'", $region) : $region
|
||||
(false !== strpos($region ?? '', "'")) ? str_replace("'", "\\'", $region) : $region
|
||||
);
|
||||
if ($regionObj) {
|
||||
return $regionObj;
|
||||
|
@ -199,7 +203,7 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
// Fall back to region identified by data-title.
|
||||
// Only apply if no double quotes exist in search string,
|
||||
// which would break the CSS selector.
|
||||
if (false === strpos($region, '"')) {
|
||||
if (false === strpos($region ?? '', '"')) {
|
||||
$regionObj = $this->getSession()->getPage()->find(
|
||||
'css',
|
||||
'[data-title="' . $region . '"]'
|
||||
|
@ -213,7 +217,7 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
if (!$this->regionMap) {
|
||||
throw new \LogicException("Cannot find 'region_map' in the behat.yml");
|
||||
}
|
||||
if (!array_key_exists($region, $this->regionMap)) {
|
||||
if (!array_key_exists($region, $this->regionMap ?? [])) {
|
||||
throw new \LogicException("Cannot find the specified region in the behat.yml");
|
||||
}
|
||||
$regionObj = $this->getSession()->getPage()->find('css', $region);
|
||||
|
@ -236,6 +240,11 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
);
|
||||
}
|
||||
|
||||
$webDriverSession = $this->getSession();
|
||||
if (!$webDriverSession->isStarted()) {
|
||||
$webDriverSession->start();
|
||||
}
|
||||
|
||||
$state = $this->getTestSessionState();
|
||||
$this->testSessionEnvironment->startTestSession($state);
|
||||
|
||||
|
@ -255,8 +264,8 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
$this->testSessionEnvironment->loadFixtureIntoDb($fixtureFile);
|
||||
}
|
||||
|
||||
if ($screenSize = getenv('BEHAT_SCREEN_SIZE')) {
|
||||
list($screenWidth, $screenHeight) = explode('x', $screenSize);
|
||||
if ($screenSize = Environment::getEnv('BEHAT_SCREEN_SIZE')) {
|
||||
list($screenWidth, $screenHeight) = explode('x', $screenSize ?? '');
|
||||
$this->getSession()->resizeWindow((int)$screenWidth, (int)$screenHeight);
|
||||
} else {
|
||||
$this->getSession()->resizeWindow(1024, 768);
|
||||
|
@ -273,6 +282,27 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @AfterStep
|
||||
*
|
||||
* Wait for all requests to be handled after each step
|
||||
*
|
||||
* @param AfterStepScope $event
|
||||
*/
|
||||
public function waitResponsesAfterStep(AfterStepScope $event)
|
||||
{
|
||||
$success = $this->testSessionEnvironment->waitForPendingRequests();
|
||||
if (!$success) {
|
||||
echo (
|
||||
'Warning! The timeout for waiting a response from the server has expired...'.PHP_EOL.
|
||||
'I keep going on, but this test behaviour may be inconsistent.'.PHP_EOL.PHP_EOL.
|
||||
'Your action required!'.PHP_EOL.PHP_EOL.
|
||||
'You may want to investigate why the server is responding that slowly.'.PHP_EOL.
|
||||
'Otherwise, you may need to increase the timeout.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a parameter map of state to set within the test session.
|
||||
* Takes TESTSESSION_PARAMS environment variable into account for run-specific configurations.
|
||||
|
@ -282,11 +312,11 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
public function getTestSessionState()
|
||||
{
|
||||
$extraParams = array();
|
||||
parse_str(getenv('TESTSESSION_PARAMS'), $extraParams);
|
||||
parse_str(Environment::getEnv('TESTSESSION_PARAMS') ?? '', $extraParams);
|
||||
return array_merge(
|
||||
array(
|
||||
'database' => $this->databaseName,
|
||||
'mailer' => 'SilverStripe\BehatExtension\Utility\TestMailer',
|
||||
'mailer' => TestMailer::class,
|
||||
),
|
||||
$extraParams
|
||||
);
|
||||
|
@ -300,13 +330,13 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
*/
|
||||
public function parseUrl($url)
|
||||
{
|
||||
$url = parse_url($url);
|
||||
$url = parse_url($url ?? '');
|
||||
$url['vars'] = array();
|
||||
if (!isset($url['fragment'])) {
|
||||
$url['fragment'] = null;
|
||||
}
|
||||
if (isset($url['query'])) {
|
||||
parse_str($url['query'], $url['vars']);
|
||||
parse_str($url['query'] ?? '', $url['vars']);
|
||||
}
|
||||
|
||||
return $url;
|
||||
|
@ -371,7 +401,7 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
|
||||
$parts = func_get_args();
|
||||
$trimSlashes = function (&$part) {
|
||||
$part = trim($part, '/');
|
||||
$part = trim($part ?? '', '/');
|
||||
};
|
||||
array_walk($parts, $trimSlashes);
|
||||
|
||||
|
@ -381,7 +411,7 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
public function canIntercept()
|
||||
{
|
||||
$driver = $this->getSession()->getDriver();
|
||||
if ($driver instanceof Selenium2Driver) {
|
||||
if ($driver instanceof FacebookWebDriver) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -409,7 +439,18 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
/** @var NodeElement $node */
|
||||
foreach ($nodes as $node) {
|
||||
if ($node->isVisible()) {
|
||||
$node->setValue($value);
|
||||
// Work around for https://github.com/FluentLenium/FluentLenium/issues/129
|
||||
// Otherwise "Element must be user-editable in order to clear it"
|
||||
$type = $node->getAttribute('type');
|
||||
$id = $node->getAttribute('id');
|
||||
if ($type === 'date' && $id) {
|
||||
$jsValue = Convert::raw2js($value);
|
||||
$this->getSession()->getDriver()->executeScript(
|
||||
"document.getElementById(\"{$id}\").value = \"{$jsValue}\";"
|
||||
);
|
||||
} else {
|
||||
$node->setValue($value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -424,7 +465,7 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
}
|
||||
|
||||
/**
|
||||
* Overwritten to click the first *visable* link the DOM.
|
||||
* Overwritten to click the first *visible* link the DOM.
|
||||
*
|
||||
* @param string $link
|
||||
* @throws ElementNotFoundException
|
||||
|
@ -469,7 +510,7 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
}
|
||||
|
||||
$state = $this->testSessionEnvironment->getState();
|
||||
$oldDatetime = \DateTime::createFromFormat('Y-m-d H:i:s', isset($state->datetime) ? $state->datetime : null);
|
||||
$oldDatetime = \DateTime::createFromFormat('Y-m-d H:i:s', $state->datetime ?? '');
|
||||
if ($oldDatetime) {
|
||||
$newDatetime->setTime($oldDatetime->format('H'), $oldDatetime->format('i'), $oldDatetime->format('s'));
|
||||
}
|
||||
|
@ -559,7 +600,7 @@ abstract class SilverStripeContext extends MinkContext implements SilverStripeAw
|
|||
$value = $field->getValue();
|
||||
$newValue = $opt->getAttribute('value');
|
||||
if (is_array($value)) {
|
||||
if (!in_array($newValue, $value)) {
|
||||
if (!in_array($newValue, $value ?? [])) {
|
||||
$value[] = $newValue;
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -17,8 +17,8 @@ trait ModuleCommandTrait
|
|||
*/
|
||||
protected function getModule($name, $error = true)
|
||||
{
|
||||
if (strpos($name, '@') === 0) {
|
||||
$name = substr($name, 1);
|
||||
if (strpos($name ?? '', '@') === 0) {
|
||||
$name = substr($name ?? '', 1);
|
||||
}
|
||||
$module = ModuleLoader::inst()->getManifest()->getModule($name);
|
||||
if (!$module && $error) {
|
||||
|
|
|
@ -138,11 +138,11 @@ class ModuleInitialisationController implements Controller
|
|||
{
|
||||
// Create feature_path
|
||||
$features = $this->container->getParameter('silverstripe_extension.context.features_path');
|
||||
$fullPath = $module->getResourcePath($features);
|
||||
if (is_dir($fullPath)) {
|
||||
$fullPath = $module->getResource($features)->getPath();
|
||||
if (is_dir($fullPath ?? '')) {
|
||||
return;
|
||||
}
|
||||
mkdir($fullPath, 0777, true);
|
||||
mkdir($fullPath ?? '', 0777, true);
|
||||
$output->writeln(
|
||||
"<info>{$fullPath}</info> - <comment>place your *.feature files here</comment>"
|
||||
);
|
||||
|
@ -164,14 +164,14 @@ class ModuleInitialisationController implements Controller
|
|||
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);
|
||||
$dirPath = $module->getResource($classesPath)->getPath();
|
||||
if (!is_dir($dirPath ?? '')) {
|
||||
mkdir($dirPath ?? '', 0777, true);
|
||||
}
|
||||
|
||||
// Scaffold base context file
|
||||
$classPath = "{$dirPath}/FeatureContext.php";
|
||||
if (is_file($classPath)) {
|
||||
if (is_file($classPath ?? '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -185,7 +185,7 @@ class ModuleInitialisationController implements Controller
|
|||
'ClassName' => $class,
|
||||
]);
|
||||
$classContent = $obj->renderWith(__DIR__.'/../../templates/FeatureContext.ss');
|
||||
file_put_contents($classPath, $classContent);
|
||||
file_put_contents($classPath ?? '', $classContent);
|
||||
|
||||
// Log
|
||||
$output->writeln(
|
||||
|
@ -193,13 +193,13 @@ class ModuleInitialisationController implements Controller
|
|||
);
|
||||
|
||||
// Add to composer json
|
||||
$composerFile = $module->getResourcePath('composer.json');
|
||||
if (!file_exists($composerFile)) {
|
||||
$composerFile = $module->getResource('composer.json')->getPath();
|
||||
if (!file_exists($composerFile ?? '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add autoload directive to composer
|
||||
$composerData = json_decode(file_get_contents($composerFile), true);
|
||||
$composerData = json_decode(file_get_contents($composerFile ?? '') ?? '', true);
|
||||
if (json_last_error()) {
|
||||
throw new Exception(json_last_error_msg());
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ class ModuleInitialisationController implements Controller
|
|||
}
|
||||
$composerData['autoload']['psr-4']["{$fullNamespace}\\"] = $classesPath;
|
||||
file_put_contents(
|
||||
$composerFile,
|
||||
$composerFile ?? '',
|
||||
json_encode($composerData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
|
||||
|
@ -239,7 +239,7 @@ class ModuleInitialisationController implements Controller
|
|||
protected function getFixtureNamespace($namespaceRoot)
|
||||
{
|
||||
$namespaceSuffix = $this->container->getParameter('silverstripe_extension.context.namespace_suffix');
|
||||
return trim($namespaceRoot, '/\\') . '\\' . $namespaceSuffix;
|
||||
return trim($namespaceRoot ?? '', '/\\') . '\\' . $namespaceSuffix;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -251,8 +251,8 @@ class ModuleInitialisationController implements Controller
|
|||
*/
|
||||
protected function initConfig($output, $module, $namespaceRoot)
|
||||
{
|
||||
$configPath = $module->getResourcePath('behat.yml');
|
||||
if (file_exists($configPath)) {
|
||||
$configPath = $module->getResource('behat.yml')->getPath();
|
||||
if (file_exists($configPath ?? '')) {
|
||||
return;
|
||||
}
|
||||
$class = $this->getFixtureClass($namespaceRoot);
|
||||
|
@ -279,7 +279,7 @@ class ModuleInitialisationController implements Controller
|
|||
]
|
||||
]
|
||||
];
|
||||
file_put_contents($configPath, Yaml::dump($data, 99999999, 2));
|
||||
file_put_contents($configPath ?? '', Yaml::dump($data, 99999999, 2));
|
||||
|
||||
$output->writeln(
|
||||
"<info>{$configPath}</info> - <comment>default behat.yml created</comment>"
|
||||
|
|
|
@ -158,7 +158,7 @@ class ModuleSuiteLocator implements Controller
|
|||
// Find all candidate paths
|
||||
foreach ([ "{$path}/", "{$path}/{$pathSuffix}"] as $parent) {
|
||||
foreach ([$parent.'behat.yml', $parent.'.behat.yml'] as $candidate) {
|
||||
if (file_exists($candidate)) {
|
||||
if (file_exists($candidate ?? '')) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
@ -178,7 +178,7 @@ class ModuleSuiteLocator implements Controller
|
|||
{
|
||||
$path = $this->findModuleConfig($module);
|
||||
$yamlParser = new Parser();
|
||||
$config = $yamlParser->parse(file_get_contents($path));
|
||||
$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");
|
||||
}
|
||||
|
|
|
@ -15,8 +15,12 @@ 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 RuntimeException;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Behat\Behat\Tester\ServiceContainer\TesterExtension;
|
||||
use SilverStripe\BehatExtension\Utility\RerunTotalStatistics;
|
||||
use SilverStripe\BehatExtension\Utility\RerunRuntimeSuiteTester;
|
||||
|
||||
/*
|
||||
* This file is part of the SilverStripe\BehatExtension
|
||||
|
@ -52,8 +56,26 @@ class Extension implements ExtensionInterface
|
|||
|
||||
public function initialize(ExtensionManager $extensionManager)
|
||||
{
|
||||
// PHPUnit
|
||||
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
|
||||
// Find PHPUnit assertion implementations
|
||||
|
||||
$found = false;
|
||||
|
||||
$options = [
|
||||
BASE_PATH . '/vendor/sminnee/phpunit/src/Framework/Assert/Functions.php',
|
||||
BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php'
|
||||
];
|
||||
|
||||
foreach ($options as $file) {
|
||||
if (file_exists($file ?? '')) {
|
||||
require_once $file;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
throw new RuntimeException('Could not find PHPUnit installation');
|
||||
}
|
||||
}
|
||||
|
||||
public function load(ContainerBuilder $container, array $config)
|
||||
|
@ -79,6 +101,22 @@ class Extension implements ExtensionInterface
|
|||
$container->setParameter('silverstripe_extension.region_map', $config['region_map']);
|
||||
}
|
||||
$container->setParameter('silverstripe_extension.bootstrap_file', $config['bootstrap_file']);
|
||||
$container->setParameter('silverstripe_extension.is_ci', $config['is_ci']);
|
||||
|
||||
// When running in CI, behat scenarios will occasionally sporadically fail
|
||||
// Replaces services with custom implementations that will rerun failed features
|
||||
// Note that features rather than scenarios need to be rerun to ensure that
|
||||
// everything is setup and torn down correctly and that "Background" bits of
|
||||
// feature fits are rerun
|
||||
if ($config['is_ci']) {
|
||||
$definition = new Definition(RerunRuntimeSuiteTester::class, array(
|
||||
new Reference(TesterExtension::SPECIFICATION_TESTER_ID)
|
||||
));
|
||||
$container->setDefinition(TesterExtension::SUITE_TESTER_ID, $definition);
|
||||
|
||||
$definition = new Definition(RerunTotalStatistics::class);
|
||||
$container->setDefinition('output.pretty.statistics', $definition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,6 +159,9 @@ class Extension implements ExtensionInterface
|
|||
info('Number of seconds that @retry tags will retry for')->
|
||||
defaultValue(2)->
|
||||
end()->
|
||||
scalarNode('is_ci')->
|
||||
defaultValue(false)->
|
||||
end()->
|
||||
arrayNode('ajax_steps')->
|
||||
defaultValue(array(
|
||||
'go to',
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace SilverStripe\BehatExtension;
|
|||
|
||||
use Behat\MinkExtension\ServiceContainer\MinkExtension as BaseMinkExtension;
|
||||
use SilverStripe\BehatExtension\Compiler\MinkExtensionBaseUrlPass;
|
||||
use SilverStripe\MinkFacebookWebDriver\FacebookFactory;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
|
@ -14,6 +15,12 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|||
*/
|
||||
class MinkExtension extends BaseMinkExtension
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->registerDriverFactory(new FacebookFactory());
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
parent::process($container);
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
<?php
|
||||
|
||||
namespace SilverStripe\BehatExtension\Utility;
|
||||
|
||||
use Behat\Behat\Hook\Scope\AfterScenarioScope;
|
||||
use Behat\Behat\Hook\Scope\AfterStepScope;
|
||||
use Behat\Behat\Hook\Scope\StepScope;
|
||||
use Behat\Testwork\Tester\Result\TestResult;
|
||||
use Facebook\WebDriver\Exception\WebDriverException;
|
||||
use SilverStripe\Assets\Filesystem;
|
||||
use SilverStripe\MinkFacebookWebDriver\FacebookWebDriver;
|
||||
|
||||
/**
|
||||
* Step tools to help debug failing steps
|
||||
*/
|
||||
trait DebugTools
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $takeScreenshotAfterEveryStep = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $dumpRenderedHtmlAfterEveryStep = false;
|
||||
|
||||
/**
|
||||
* Ensure utilty steps are reset for subsequent scenarios
|
||||
*
|
||||
* @AfterScenario
|
||||
* @param AfterScenarioScope $event
|
||||
*/
|
||||
public function resetUtilitiesAfterStep(AfterScenarioScope $event): void
|
||||
{
|
||||
$this->takeScreenshotAfterEveryStep = false;
|
||||
$this->dumpRenderedHtmlAfterEveryStep = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful step for working out why a behat testing isn't working when running
|
||||
* the browser headless
|
||||
* Remove this step from in a feature file once the test is working correct
|
||||
*
|
||||
* @Given /^I take a screenshot after every step$/
|
||||
*/
|
||||
public function iTakeAScreenshotAfterEveryStep()
|
||||
{
|
||||
$this->takeScreenshotAfterEveryStep = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for debugging failing behat tests
|
||||
* Remove this step from in a feature file once the test is working correct
|
||||
*
|
||||
* @Given /^I dump the rendered HTML after every step$/
|
||||
*/
|
||||
public function iDumpTheRenderedHtmlAfterEveryStep()
|
||||
{
|
||||
$this->dumpRenderedHtmlAfterEveryStep = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a screenshot when step fails, or
|
||||
* take a screenshot after every step if the use has specified
|
||||
* "I take a screenshot after every step"
|
||||
* Works only with FacebookWebDriver.
|
||||
*
|
||||
* @AfterStep
|
||||
* @param AfterStepScope $event
|
||||
*/
|
||||
public function takeScreenshotAfterFailedStep(AfterStepScope $event)
|
||||
{
|
||||
// Check failure code
|
||||
if (!$this->takeScreenshotAfterEveryStep && $event->getTestResult()->getResultCode() !== TestResult::FAILED) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->takeScreenshot($event);
|
||||
} catch (WebDriverException $e) {
|
||||
$this->logException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump HTML when step fails.
|
||||
*
|
||||
* @AfterStep
|
||||
* @param AfterStepScope $event
|
||||
*/
|
||||
public function dumpHtmlAfterStep(AfterStepScope $event): void
|
||||
{
|
||||
// Check failure code
|
||||
if ($event->getTestResult()->getResultCode() !== TestResult::FAILED && !$this->dumpRenderedHtmlAfterEveryStep) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->dumpRenderedHtml($event);
|
||||
} catch (WebDriverException $e) {
|
||||
$this->logException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump rendered HTML to disk
|
||||
* Useful for seeing the state of a page when writing and debugging feature files
|
||||
*
|
||||
* @param StepScope $event
|
||||
*/
|
||||
public function dumpRenderedHtml(StepScope $event)
|
||||
{
|
||||
$feature = $event->getFeature();
|
||||
$step = $event->getStep();
|
||||
$path = $this->prepareScreenshotPath();
|
||||
if (!$path) {
|
||||
return;
|
||||
}
|
||||
// prefix with zz_ so that it alpha sorts in the directory lower than screenshots which
|
||||
// will typically be referred to far more often. This is mainly for when you have
|
||||
// enabled `dumpRenderedHtmlAfterEveryStep`
|
||||
$path = sprintf('%s/zz_%s_%d.html', $path, basename($feature->getFile() ?? ''), $step->getLine());
|
||||
$html = $this->getSession()->getPage()->getOuterHtml();
|
||||
file_put_contents($path ?? '', $html);
|
||||
$this->logMessage(sprintf('Saving HTML into %s', $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a nice screenshot
|
||||
*
|
||||
* @param StepScope $event
|
||||
*/
|
||||
public function takeScreenshot(StepScope $event)
|
||||
{
|
||||
// Validate driver
|
||||
$driver = $this->getSession()->getDriver();
|
||||
$feature = $event->getFeature();
|
||||
$step = $event->getStep();
|
||||
$path = $this->prepareScreenshotPath();
|
||||
if (!$path) {
|
||||
return;
|
||||
}
|
||||
$path = sprintf('%s/%s_%d.png', $path, basename($feature->getFile() ?? ''), $step->getLine());
|
||||
$screenshot = $driver->getScreenshot();
|
||||
file_put_contents($path ?? '', $screenshot);
|
||||
$this->logMessage(sprintf('Saving screenshot into %s', $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the screenshots path is created
|
||||
*/
|
||||
private function prepareScreenshotPath()
|
||||
{
|
||||
// Check paths are configured
|
||||
$path = $this->getMainContext()->getScreenshotPath();
|
||||
if (!$path) {
|
||||
$this->logMessage('ScreenShots path not configured: skipping');
|
||||
return;
|
||||
}
|
||||
Filesystem::makeFolder($path);
|
||||
$path = realpath($path ?? '');
|
||||
if (!file_exists($path ?? '')) {
|
||||
$this->logMessage(sprintf('"%s" is not valid directory and failed to create it', $path));
|
||||
return;
|
||||
}
|
||||
if (file_exists($path ?? '') && !is_dir($path ?? '')) {
|
||||
$this->logMessage(sprintf('"%s" is not valid directory', $path));
|
||||
return;
|
||||
}
|
||||
if (file_exists($path ?? '') && !is_writable($path ?? '')) {
|
||||
$this->logMessage(sprintf('"%s" directory is not writable', $path));
|
||||
return;
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace SilverStripe\BehatExtension\Utility;
|
||||
|
||||
use Behat\Testwork\Environment\Environment;
|
||||
use Behat\Testwork\Specification\SpecificationIterator;
|
||||
use Behat\Testwork\Tester\Result\IntegerTestResult;
|
||||
use Behat\Testwork\Tester\Result\TestResult;
|
||||
use Behat\Testwork\Tester\Result\TestResults;
|
||||
use Behat\Testwork\Tester\Result\TestWithSetupResult;
|
||||
use Behat\Testwork\Tester\Setup\SuccessfulSetup;
|
||||
use Behat\Testwork\Tester\Setup\SuccessfulTeardown;
|
||||
use Behat\Testwork\Tester\SpecificationTester;
|
||||
use Behat\Testwork\Tester\SuiteTester;
|
||||
|
||||
/**
|
||||
* Copy paste of Behat\Testwork\Tester\Runtime\RuntimeSuiteTester which is a final class
|
||||
*
|
||||
* Modified so that it reruns failed features
|
||||
*/
|
||||
class RerunRuntimeSuiteTester implements SuiteTester
|
||||
{
|
||||
/**
|
||||
* @var SpecificationTester
|
||||
*/
|
||||
private $specTester;
|
||||
|
||||
/**
|
||||
* Initializes tester.
|
||||
*
|
||||
* @param SpecificationTester $specTester
|
||||
*/
|
||||
public function __construct(SpecificationTester $specTester)
|
||||
{
|
||||
$this->specTester = $specTester;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp(Environment $env, SpecificationIterator $iterator, $skip)
|
||||
{
|
||||
return new SuccessfulSetup();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function test(Environment $env, SpecificationIterator $iterator, $skip = false)
|
||||
{
|
||||
$results = array();
|
||||
foreach ($iterator as $specification) {
|
||||
$setup = $this->specTester->setUp($env, $specification, $skip);
|
||||
$localSkip = !$setup->isSuccessful() || $skip;
|
||||
$testResult = $this->specTester->test($env, $specification, $localSkip);
|
||||
$teardown = $this->specTester->tearDown($env, $specification, $localSkip, $testResult);
|
||||
|
||||
// start modifications here
|
||||
if (!$testResult->isPassed()) {
|
||||
file_put_contents('php://stdout', 'Retrying specification' . PHP_EOL);
|
||||
$setup = $this->specTester->setUp($env, $specification, $skip);
|
||||
$localSkip = !$setup->isSuccessful() || $skip;
|
||||
$testResult = $this->specTester->test($env, $specification, $localSkip);
|
||||
$teardown = $this->specTester->tearDown($env, $specification, $localSkip, $testResult);
|
||||
}
|
||||
// end modifications here
|
||||
|
||||
$integerResult = new IntegerTestResult($testResult->getResultCode());
|
||||
$results[] = new TestWithSetupResult($setup, $integerResult, $teardown);
|
||||
}
|
||||
|
||||
return new TestResults($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function tearDown(Environment $env, SpecificationIterator $iterator, $skip, TestResult $result)
|
||||
{
|
||||
return new SuccessfulTeardown();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,314 @@
|
|||
<?php
|
||||
|
||||
namespace SilverStripe\BehatExtension\Utility;
|
||||
|
||||
use Behat\Behat\Tester\Result\StepResult;
|
||||
use Behat\Testwork\Counter\Memory;
|
||||
use Behat\Testwork\Counter\Timer;
|
||||
use Behat\Testwork\Tester\Result\TestResult;
|
||||
use Behat\Testwork\Tester\Result\TestResults;
|
||||
use Behat\Behat\Output\Statistics\Statistics;
|
||||
use Behat\Behat\Output\Statistics\ScenarioStat;
|
||||
use Behat\Behat\Output\Statistics\StepStat;
|
||||
use Behat\Behat\Output\Statistics\HookStat;
|
||||
|
||||
/**
|
||||
* Copy paste of Behat\Behat\Output\Statistics\TotalStatistics which is a final class
|
||||
*
|
||||
* Modified to remove duplicated stats from reruns
|
||||
*/
|
||||
class RerunTotalStatistics implements Statistics
|
||||
{
|
||||
/**
|
||||
* @var Timer
|
||||
*/
|
||||
private $timer;
|
||||
/**
|
||||
* @var Memory
|
||||
*/
|
||||
private $memory;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $scenarioCounters = array();
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $stepCounters = array();
|
||||
/**
|
||||
* @var ScenarioStat[]
|
||||
*/
|
||||
private $failedScenarioStats = array();
|
||||
/**
|
||||
* @var ScenarioStat[]
|
||||
*/
|
||||
private $skippedScenarioStats = array();
|
||||
/**
|
||||
* @var StepStat[]
|
||||
*/
|
||||
private $failedStepStats = array();
|
||||
/**
|
||||
* @var StepStat[]
|
||||
*/
|
||||
private $pendingStepStats = array();
|
||||
/**
|
||||
* @var HookStat[]
|
||||
*/
|
||||
private $failedHookStats = array();
|
||||
// start modifications here
|
||||
/**
|
||||
* @var StepStat[]
|
||||
*/
|
||||
private $passedStepStats = array();
|
||||
// end modifications here
|
||||
|
||||
/**
|
||||
* Initializes statistics.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->resetAllCounters();
|
||||
|
||||
$this->timer = new Timer();
|
||||
$this->memory = new Memory();
|
||||
}
|
||||
|
||||
public function resetAllCounters()
|
||||
{
|
||||
$this->scenarioCounters = $this->stepCounters = array(
|
||||
TestResult::PASSED => 0,
|
||||
TestResult::FAILED => 0,
|
||||
StepResult::UNDEFINED => 0,
|
||||
TestResult::PENDING => 0,
|
||||
TestResult::SKIPPED => 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts timer.
|
||||
*/
|
||||
public function startTimer()
|
||||
{
|
||||
$this->timer->start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops timer.
|
||||
*/
|
||||
public function stopTimer()
|
||||
{
|
||||
$this->timer->stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns timer object.
|
||||
*
|
||||
* @return Timer
|
||||
*/
|
||||
public function getTimer()
|
||||
{
|
||||
return $this->timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns memory usage object.
|
||||
*
|
||||
* @return Memory
|
||||
*/
|
||||
public function getMemory()
|
||||
{
|
||||
return $this->memory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers scenario stat.
|
||||
*
|
||||
* @param ScenarioStat $stat
|
||||
*/
|
||||
public function registerScenarioStat(ScenarioStat $stat)
|
||||
{
|
||||
if (TestResults::NO_TESTS === $stat->getResultCode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->scenarioCounters[$stat->getResultCode()]++;
|
||||
|
||||
// start modifications here
|
||||
if (TestResult::FAILED === $stat->getResultCode()) {
|
||||
// Ensure that any scenario reruns aren't counted as additional failures
|
||||
$alreadyHasFailure = false;
|
||||
foreach ($this->failedScenarioStats as $failedStat) {
|
||||
if ($failedStat->getPath() === $stat->getPath()) {
|
||||
$alreadyHasFailure = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$alreadyHasFailure) {
|
||||
$this->failedScenarioStats[] = $stat;
|
||||
} else {
|
||||
$this->scenarioCounters[TestResult::FAILED]--;
|
||||
}
|
||||
}
|
||||
|
||||
if (TestResult::PASSED == $stat->getResultCode()) {
|
||||
// Remove the scenario from the failed scenarios list if it passes on rerun
|
||||
$newFailedScenarioStats = [];
|
||||
foreach ($this->failedScenarioStats as $failedStat) {
|
||||
if ($failedStat->getPath() !== $stat->getPath()) {
|
||||
$newFailedScenarioStats[] = $failedStat;
|
||||
} else {
|
||||
$this->scenarioCounters[TestResult::FAILED]--;
|
||||
}
|
||||
}
|
||||
$this->failedScenarioStats = $newFailedScenarioStats;
|
||||
}
|
||||
// end modifications here
|
||||
|
||||
if (TestResult::SKIPPED === $stat->getResultCode()) {
|
||||
$this->skippedScenarioStats[] = $stat;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers step stat.
|
||||
*
|
||||
* @param StepStat $stat
|
||||
*/
|
||||
public function registerStepStat(StepStat $stat)
|
||||
{
|
||||
$this->stepCounters[$stat->getResultCode()]++;
|
||||
|
||||
// start modifications here
|
||||
if (TestResult::FAILED === $stat->getResultCode()) {
|
||||
// Ensure that any scenario reruns don't double count step failures
|
||||
$alreadyHasFailure = false;
|
||||
foreach ($this->failedStepStats as $failedStat) {
|
||||
if ($failedStat->getPath() === $stat->getPath()) {
|
||||
$alreadyHasFailure = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$alreadyHasFailure) {
|
||||
$this->failedStepStats[] = $stat;
|
||||
} else {
|
||||
$this->stepCounters[TestResult::FAILED]--;
|
||||
}
|
||||
}
|
||||
|
||||
if (TestResult::PASSED == $stat->getResultCode()) {
|
||||
// Remove any duplicate passes on scenario rerun
|
||||
$alreadyHasSuccess = false;
|
||||
foreach ($this->passedStepStats as $passedStat) {
|
||||
if ($passedStat->getPath() === $stat->getPath()) {
|
||||
$alreadyHasSuccess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$alreadyHasSuccess) {
|
||||
$this->passedStepStats[] = $stat;
|
||||
} else {
|
||||
$this->stepCounters[TestResult::PASSED]--;
|
||||
}
|
||||
|
||||
// Remove the step from the failed steps list if it passes on scenario rerun
|
||||
$newFailedStepStats = [];
|
||||
foreach ($this->failedStepStats as $failedStat) {
|
||||
if ($failedStat->getPath() !== $stat->getPath()) {
|
||||
$newFailedStepStats[] = $failedStat;
|
||||
} else {
|
||||
$this->stepCounters[TestResult::FAILED]--;
|
||||
}
|
||||
}
|
||||
$this->failedStepStats = $newFailedStepStats;
|
||||
}
|
||||
// end modifications here
|
||||
|
||||
if (TestResult::PENDING === $stat->getResultCode()) {
|
||||
$this->pendingStepStats[] = $stat;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers hook stat.
|
||||
*
|
||||
* @param HookStat $stat
|
||||
*/
|
||||
public function registerHookStat(HookStat $stat)
|
||||
{
|
||||
if ($stat->isSuccessful()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->failedHookStats[] = $stat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns counters for different scenario result codes.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function getScenarioStatCounts()
|
||||
{
|
||||
return $this->scenarioCounters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns skipped scenario stats.
|
||||
*
|
||||
* @return ScenarioStat[]
|
||||
*/
|
||||
public function getSkippedScenarios()
|
||||
{
|
||||
return $this->skippedScenarioStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns failed scenario stats.
|
||||
*
|
||||
* @return ScenarioStat[]
|
||||
*/
|
||||
public function getFailedScenarios()
|
||||
{
|
||||
return $this->failedScenarioStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns counters for different step result codes.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function getStepStatCounts()
|
||||
{
|
||||
return $this->stepCounters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns failed step stats.
|
||||
*
|
||||
* @return StepStat[]
|
||||
*/
|
||||
public function getFailedSteps()
|
||||
{
|
||||
return $this->failedStepStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns pending step stats.
|
||||
*
|
||||
* @return StepStat[]
|
||||
*/
|
||||
public function getPendingSteps()
|
||||
{
|
||||
return $this->pendingStepStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns failed hook stats.
|
||||
*
|
||||
* @return HookStat[]
|
||||
*/
|
||||
public function getFailedHookStats()
|
||||
{
|
||||
return $this->failedHookStats;
|
||||
}
|
||||
}
|
|
@ -112,10 +112,10 @@ class RetryableCallHandler implements CallHandler
|
|||
// Determine whether to call with retries
|
||||
if ($retry) {
|
||||
$return = $this->retryThrowable(function () use ($callable, $arguments) {
|
||||
return call_user_func_array($callable, $arguments);
|
||||
return call_user_func_array($callable, $arguments ?? []);
|
||||
}, $this->retrySeconds);
|
||||
} else {
|
||||
$return = call_user_func_array($callable, $arguments);
|
||||
$return = call_user_func_array($callable, $arguments ?? []);
|
||||
}
|
||||
} catch (Exception $caught) {
|
||||
$exception = $caught;
|
||||
|
@ -144,7 +144,7 @@ class RetryableCallHandler implements CallHandler
|
|||
private function startErrorAndOutputBuffering(Call $call)
|
||||
{
|
||||
$errorReporting = $call->getErrorReportingLevel() ? : $this->errorReportingLevel;
|
||||
set_error_handler(array($this, 'handleError'), $errorReporting);
|
||||
set_error_handler(array($this, 'handleError'), $errorReporting ?? 0);
|
||||
$this->obStarted = ob_start();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,14 @@
|
|||
|
||||
namespace SilverStripe\BehatExtension\Utility;
|
||||
|
||||
use Behat\Behat\Hook\Scope\ScenarioScope;
|
||||
use Behat\Behat\Hook\Scope\StepScope;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\Node\NodeInterface;
|
||||
use Behat\Gherkin\Node\ScenarioInterface;
|
||||
use Behat\Gherkin\Node\TaggedNodeInterface;
|
||||
use \Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Helpers for working with steps
|
||||
|
@ -65,22 +68,35 @@ trait StepHelper
|
|||
/**
|
||||
* Check if a step has a given tag
|
||||
*
|
||||
* @param StepScope $event
|
||||
* @param StepScope|ScenarioScope $event
|
||||
* @param string $tag
|
||||
* @return bool
|
||||
*/
|
||||
protected function stepHasTag(StepScope $event, $tag)
|
||||
protected function stepHasTag($event, $tag)
|
||||
{
|
||||
// Check feature
|
||||
$feature = $event->getFeature();
|
||||
if ($feature && $feature->hasTag($tag)) {
|
||||
return true;
|
||||
$checks = [];
|
||||
if ($event instanceof StepScope) {
|
||||
$checks[] = $feature = $event->getFeature();
|
||||
$checks[] = $this->getStepScenario($feature, $event->getStep());
|
||||
} elseif ($event instanceof ScenarioScope) {
|
||||
$checks[] = $event->getFeature();
|
||||
$checks[] = $event->getScenario();
|
||||
} else {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'%s expected an instance of either %s or %s. Got %s instead',
|
||||
__METHOD__,
|
||||
StepScope::class,
|
||||
ScenarioScope::class,
|
||||
is_object($event) ? sprintf('an instance of %s', get_class($event)) : gettype($event)
|
||||
));
|
||||
}
|
||||
// Check scenario
|
||||
$scenario = $this->getStepScenario($feature, $event->getStep());
|
||||
if ($scenario && $scenario->hasTag($tag)) {
|
||||
return true;
|
||||
|
||||
foreach ($checks as $check) {
|
||||
if ($check instanceof TaggedNodeInterface && $check->hasTag($tag)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ class TestMailer extends BaseTestMailer
|
|||
{
|
||||
$matches = $this->findEmails($to, $from, $subject, $content);
|
||||
//got the count of matches emails
|
||||
$emailCount = count($matches);
|
||||
$emailCount = count($matches ?? []);
|
||||
//get the last(latest) one
|
||||
return $matches ? $matches[$emailCount-1] : null;
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ class TestMailer extends BaseTestMailer
|
|||
$value = (isset($args[$i])) ? $args[$i] : null;
|
||||
if ($value) {
|
||||
if ($value[0] == '/') {
|
||||
$matched = preg_match($value, $email->$field);
|
||||
$matched = preg_match($value ?? '', $email->$field ?? '');
|
||||
} else {
|
||||
$matched = ($value == $email->$field);
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ class TestMailer extends BaseTestMailer
|
|||
if (!isset($state->emails)) {
|
||||
$state->emails = array();
|
||||
}
|
||||
$state->emails[] = array_filter($data);
|
||||
$state->emails[] = array_filter($data ?? []);
|
||||
$this->testSessionEnvironment->applyState($state);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,4 @@ default:
|
|||
selenium2:
|
||||
browser: firefox
|
||||
SilverStripe\BehatExtension\Extension:
|
||||
screenshot_path: %paths.base%/artifacts/screenshots
|
||||
screenshot_path: '%paths.base%/artifacts/screenshots'
|
||||
|
|
|
@ -8,37 +8,27 @@ use Behat\Mink\Session;
|
|||
use Behat\Mink\Mink;
|
||||
use Behat\Mink\Driver\DriverInterface;
|
||||
use Behat\Mink\Element\Element;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use SilverStripe\BehatExtension\Tests\SilverStripeContextTest\FeatureContext;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
|
||||
class SilverStripeContextTest extends \PHPUnit_Framework_TestCase
|
||||
class SilverStripeContextTest extends SapphireTest
|
||||
{
|
||||
|
||||
protected $backupGlobals = false;
|
||||
|
||||
public static function setUpBeforeClass()
|
||||
{
|
||||
// Bootstrap test environment
|
||||
parent::setUpBeforeClass();
|
||||
SapphireTest::start();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \LogicException
|
||||
* @expectedExceptionMessage Cannot find 'region_map' in the behat.yml
|
||||
*/
|
||||
public function testGetRegionObjThrowsExceptionOnUnknownSelector()
|
||||
{
|
||||
$this->expectException(\LogicException::class);
|
||||
$this->expectExceptionMessage("Cannot find 'region_map' in the behat.yml");
|
||||
$context = $this->getContextMock();
|
||||
$context->getRegionObj('.unknown');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \LogicException
|
||||
* @expectedExceptionMessage Cannot find the specified region in the behat.yml
|
||||
*/
|
||||
public function testGetRegionObjThrowsExceptionOnUnknownRegion()
|
||||
{
|
||||
$this->expectException(\LogicException::class);
|
||||
$this->expectExceptionMessage("Cannot find the specified region in the behat.yml");
|
||||
$context = $this->getContextMock();
|
||||
$context->setRegionMap(array('MyRegion' => '.my-region'));
|
||||
$context->getRegionObj('.unknown');
|
||||
|
@ -99,7 +89,7 @@ class SilverStripeContextTest extends \PHPUnit_Framework_TestCase
|
|||
}
|
||||
|
||||
/**
|
||||
* @return Element|\PHPUnit_Framework_MockObject_MockObject
|
||||
* @return Element|MockObject
|
||||
*/
|
||||
protected function getElementMock()
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue