Completed
Push — master ( 700e53...676b2a )
by Ingo
02:17
created

SilverStripeContext::visit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
rs 9.6667
cc 1
eloc 5
nc 1
nop 1
1
<?php
2
3
namespace SilverStripe\BehatExtension\Context;
4
5
use Behat\Behat\Context\Step,
6
	Behat\Behat\Event\FeatureEvent,
7
	Behat\Behat\Event\ScenarioEvent,
8
	Behat\Behat\Event\SuiteEvent;
9
use Behat\Gherkin\Node\PyStringNode;
10
use Behat\MinkExtension\Context\MinkContext;
11
use Behat\Mink\Driver\GoutteDriver,
12
	Behat\Mink\Driver\Selenium2Driver,
13
	Behat\Mink\Exception\UnsupportedDriverActionException,
14
	Behat\Mink\Exception\ElementNotFoundException;
15
16
use SilverStripe\BehatExtension\Context\SilverStripeAwareContextInterface;
17
18
use Symfony\Component\Yaml\Yaml;
19
20
// Mink etc.
21
require_once 'vendor/autoload.php';
22
23
/**
24
 * SilverStripeContext
25
 *
26
 * Generic context wrapper used as a base for Behat FeatureContext.
27
 */
28
class SilverStripeContext extends MinkContext implements SilverStripeAwareContextInterface
29
{
30
	protected $databaseName;
31
32
	/**
33
	 * @var Array Partial string match for step names
34
	 * that are considered to trigger Ajax request in the CMS,
35
	 * and hence need special timeout handling.
36
	 * @see \SilverStripe\BehatExtension\Context\BasicContext->handleAjaxBeforeStep().
37
	 */
38
	protected $ajaxSteps;
39
40
	/**
41
	 * @var Int Timeout in milliseconds, after which the interface assumes
42
	 * that an Ajax request has timed out, and continues with assertions.
43
	 */
44
	protected $ajaxTimeout;
45
46
	/**
47
	 * @var String Relative URL to the SilverStripe admin interface.
48
	 */
49
	protected $adminUrl;
50
51
	/**
52
	 * @var String Relative URL to the SilverStripe login form.
53
	 */
54
	protected $loginUrl;
55
56
	/**
57
	 * @var String Relative path to a writeable folder where screenshots can be stored.
58
	 * If set to NULL, no screenshots will be stored.
59
	 */
60
	protected $screenshotPath;
61
62
	protected $context;
63
64
	protected $testSessionEnvironment;
65
66
67
	/**
68
	 * Initializes context.
69
	 * Every scenario gets it's own context object.
70
	 *
71
	 * @param   array   $parameters     context parameters (set them up through behat.yml)
72
	 */
73
	public function __construct(array $parameters) {
74
		// Initialize your context here
75
		$this->context = $parameters;
76
		$this->testSessionEnvironment = new \TestSessionEnvironment();
77
	}
78
79
	public function setDatabase($databaseName) {
80
		$this->databaseName = $databaseName;
81
	}
82
83
	public function setAjaxSteps($ajaxSteps) {
84
		if($ajaxSteps) $this->ajaxSteps = $ajaxSteps;
85
	}
86
87
	public function getAjaxSteps() {
88
		return $this->ajaxSteps;
89
	}
90
91
	public function setAjaxTimeout($ajaxTimeout) {
92
		$this->ajaxTimeout = $ajaxTimeout;
93
	}
94
95
	public function getAjaxTimeout() {
96
		return $this->ajaxTimeout;
97
	}
98
99
	public function setAdminUrl($adminUrl) {
100
		$this->adminUrl = $adminUrl;
101
	}
102
103
	public function getAdminUrl() {
104
		return $this->adminUrl;
105
	}
106
107
	public function setLoginUrl($loginUrl) {
108
		$this->loginUrl = $loginUrl;
109
	}
110
111
	public function getLoginUrl() {
112
		return $this->loginUrl;
113
	}
114
115
	public function setScreenshotPath($screenshotPath) {
116
		$this->screenshotPath = $screenshotPath;
117
	}
118
119
	public function getScreenshotPath() {
120
		return $this->screenshotPath;
121
	}
122
123
	public function getRegionMap(){
124
		return $this->regionMap;
0 ignored issues
show
Bug introduced by
The property regionMap does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
125
	}
126
127
	public function setRegionMap($regionMap){
128
		$this->regionMap = $regionMap;
129
	}
130
131
	/**
132
	 * Returns MinkElement based off region defined in .yml file.
133
	 * Also supports direct CSS selectors and regions identified by a "data-title" attribute.
134
	 * When using the "data-title" attribute, ensure not to include double quotes.
135
	 *
136
	 * @param String $region Region name or CSS selector
137
	 * @return MinkElement|null
138
	 */
139
	public function getRegionObj($region) {
140
		// Try to find regions directly by CSS selector.
141
		try {
142
			$regionObj = $this->getSession()->getPage()->find(
143
				'css',
144
				// Escape CSS selector
145
				(false !== strpos($region, "'")) ? str_replace("'", "\'", $region) : $region
146
			);
147
			if($regionObj) {
148
				return $regionObj;
149
			}
150
		} catch(\Symfony\Component\CssSelector\Exception\SyntaxErrorException $e) {
151
			// fall through to next case
152
		}
153
154
		// Fall back to region identified by data-title.
155
		// Only apply if no double quotes exist in search string,
156
		// which would break the CSS selector.
157
		if(false === strpos($region, '"')) {
158
			$regionObj = $this->getSession()->getPage()->find(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $regionObj is correct as $this->getSession()->get...le="' . $region . '"]') (which targets Behat\Mink\Element\Element::find()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
159
				'css',
160
				'[data-title="' . $region . '"]'
161
			);
162
			if($regionObj) {
163
				return $regionObj;
164
			}
165
		}
166
167
		// Look for named region
168
		if(!$this->regionMap) {
169
			throw new \LogicException("Cannot find 'region_map' in the behat.yml");
170
		}
171
		if(!array_key_exists($region, $this->regionMap)) {
172
			throw new \LogicException("Cannot find the specified region in the behat.yml");
173
		}
174
		$regionObj = $this->getSession()->getPage()->find('css', $region);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $regionObj is correct as $this->getSession()->get...)->find('css', $region) (which targets Behat\Mink\Element\Element::find()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
175
		if(!$regionObj) {
176
			throw new ElementNotFoundException("Cannot find the specified region on the page");
0 ignored issues
show
Documentation introduced by
'Cannot find the specified region on the page' is of type string, but the function expects a object<Behat\Mink\Session>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
177
		}
178
179
		return $regionObj;
180
	}
181
182
	/**
183
	 * @BeforeScenario
184
	 */
185
	public function before(ScenarioEvent $event) {
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
186
		if (!isset($this->databaseName)) {
187
			throw new \LogicException(
188
				'Context\'s $databaseName has to be set when implementing SilverStripeAwareContextInterface.'
189
			);
190
		}
191
192
		$state = $this->getTestSessionState();
193
		$this->testSessionEnvironment->startTestSession($state);
194
195
		// Optionally import database
196
		if(!empty($state['importDatabasePath'])) {
197
			$this->testSessionEnvironment->importDatabase(
198
				$state['importDatabasePath'],
199
				!empty($state['requireDefaultRecords']) ? $state['requireDefaultRecords'] : false
200
			);
201
		} else if(!empty($state['requireDefaultRecords']) && $state['requireDefaultRecords']) {
202
			$this->testSessionEnvironment->requireDefaultRecords();
203
		}
204
205
		// Fixtures
206
		$fixtureFile = (!empty($state['fixture'])) ? $state['fixture'] : null;
207
		if($fixtureFile) {
208
			$this->testSessionEnvironment->loadFixtureIntoDb($fixtureFile);
209
		}
210
211
		if($screenSize = getenv('BEHAT_SCREEN_SIZE')) {
212
			list($screenWidth, $screenHeight) = explode('x', $screenSize);
213
			$this->getSession()->resizeWindow((int)$screenWidth, (int)$screenHeight);
214
		} else {
215
			$this->getSession()->resizeWindow(1024, 768);
216
		}
217
	}
218
219
	/**
220
	 * Returns a parameter map of state to set within the test session.
221
	 * Takes TESTSESSION_PARAMS environment variable into account for run-specific configurations.
222
	 *
223
	 * @return array
224
	 */
225
	public function getTestSessionState() {
226
		$extraParams = array();
227
		parse_str(getenv('TESTSESSION_PARAMS'), $extraParams);
228
		return array_merge(
229
			array(
230
				'database' => $this->databaseName,
231
				'mailer' => 'SilverStripe\BehatExtension\Utility\TestMailer',
232
			),
233
			$extraParams
234
		);
235
	}
236
237
	/**
238
	 * Parses given URL and returns its components
239
	 *
240
	 * @param $url
241
	 * @return array|mixed Parsed URL
242
	 */
243
	public function parseUrl($url) {
244
		$url = parse_url($url);
245
		$url['vars'] = array();
246
		if (!isset($url['fragment'])) {
247
			$url['fragment'] = null;
248
		}
249
		if (isset($url['query'])) {
250
			parse_str($url['query'], $url['vars']);
251
		}
252
253
		return $url;
254
	}
255
256
	/**
257
	 * Checks whether current URL is close enough to the given URL.
258
	 * Unless specified in $url, get vars will be ignored
259
	 * Unless specified in $url, fragment identifiers will be ignored
260
	 *
261
	 * @param $url string URL to compare to current URL
262
	 * @return boolean Returns true if the current URL is close enough to the given URL, false otherwise.
263
	 */
264
	public function isCurrentUrlSimilarTo($url) {
265
		$current = $this->parseUrl($this->getSession()->getCurrentUrl());
266
		$test = $this->parseUrl($url);
267
268
		if ($current['path'] !== $test['path']) {
269
			return false;
270
		}
271
272
		if (isset($test['fragment']) && $current['fragment'] !== $test['fragment']) {
273
			return false;
274
		}
275
276
		foreach ($test['vars'] as $name => $value) {
277
			if (!isset($current['vars'][$name]) || $current['vars'][$name] !== $value) {
278
				return false;
279
			}
280
		}
281
282
		return true;
283
	}
284
285
	/**
286
	 * Returns base URL parameter set in MinkExtension.
287
	 * It simplifies configuration by allowing to specify this parameter
288
	 * once but makes code dependent on MinkExtension.
289
	 *
290
	 * @return string
291
	 */
292
	public function getBaseUrl() {
293
		return $this->getMinkParameter('base_url') ?: '';
294
	}
295
296
	/**
297
	 * Joins URL parts into an URL using forward slash.
298
	 * Forward slash usages are normalised to one between parts.
299
	 * This method takes variable number of parameters.
300
	 *
301
	 * @param $...
302
	 * @return string
303
	 * @throws \InvalidArgumentException
304
	 */
305
	public function joinUrlParts() {
306
		if (0 === func_num_args()) {
307
			throw new \InvalidArgumentException('Need at least one argument');
308
		}
309
310
		$parts = func_get_args();
311
		$trimSlashes = function(&$part) {
312
			$part = trim($part, '/');
313
		};
314
		array_walk($parts, $trimSlashes);
315
316
		return implode('/', $parts);
317
	}
318
319
	public function canIntercept() {
320
		$driver = $this->getSession()->getDriver();
321
		if ($driver instanceof GoutteDriver) {
322
			return true;
323
		}
324
		else {
325
			if ($driver instanceof Selenium2Driver) {
326
				return false;
327
			}
328
		}
329
330
		throw new UnsupportedDriverActionException('You need to tag the scenario with "@mink:goutte" or
331
			"@mink:symfony". Intercepting the redirections is not supported by %s', $driver);
332
	}
333
334
	/**
335
	 * @Given /^(.*) without redirection$/
336
	 */
337
	public function theRedirectionsAreIntercepted($step) {
338
		if ($this->canIntercept()) {
339
			$this->getSession()->getDriver()->getClient()->followRedirects(false);
0 ignored issues
show
Bug introduced by
The method getClient() does not seem to exist on object<Behat\Mink\Driver\DriverInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
340
		}
341
342
		return new Step\Given($step);
343
	}
344
345
	/**
346
	 * Fills in form field with specified id|name|label|value.
347
	 * Overwritten to select the first *visible* element, see https://github.com/Behat/Mink/issues/311
348
	 */
349 View Code Duplication
	public function fillField($field, $value) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
350
		$value = $this->fixStepArgument($value);
351
		$fields = $this->getSession()->getPage()->findAll('named', array(
352
			'field', $this->getSession()->getSelectorsHandler()->xpathLiteral($field)
353
		));
354
		if($fields) foreach($fields as $f) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type Behat\Mink\Element\NodeElement[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
355
			if($f->isVisible()) {
356
				$f->setValue($value);
357
				return;
358
			}
359
		}
360
361
		throw new ElementNotFoundException(
362
			$this->getSession(), 'form field', 'id|name|label|value', $field
363
		);
364
	}
365
366
	/**
367
	 * Overwritten to click the first *visable* link the DOM.
368
	 */
369 View Code Duplication
	public function clickLink($link) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
370
		$link = $this->fixStepArgument($link);
371
		$links = $this->getSession()->getPage()->findAll('named', array(
372
			'link', $this->getSession()->getSelectorsHandler()->xpathLiteral($link)
373
		));
374
		if($links) foreach($links as $l) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $links of type Behat\Mink\Element\NodeElement[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
375
			if($l->isVisible()) {
376
				$l->click();
377
				return;
378
			}
379
		}
380
		throw new ElementNotFoundException(
381
			$this->getSession(), 'link', 'id|name|label|value', $link
382
		);
383
	}
384
385
	 /**
386
	 * Sets the current date. Relies on the underlying functionality using
387
	 * {@link SS_Datetime::now()} rather than PHP's system time methods like date().
388
	 * Supports ISO fomat: Y-m-d
389
	 * Example: Given the current date is "2009-10-31"
390
	 *
391
	 * @Given /^the current date is "([^"]*)"$/
392
	 */
393 View Code Duplication
	public function givenTheCurrentDateIs($date) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
394
		$newDatetime = \DateTime::createFromFormat('Y-m-d', $date);
395
		if(!$newDatetime) {
396
			throw new InvalidArgumentException(sprintf('Invalid date format: %s (requires "Y-m-d")', $date));
397
		}
398
399
		$state = $this->testSessionEnvironment->getState();
400
		$oldDatetime = \DateTime::createFromFormat('Y-m-d H:i:s', isset($state->datetime) ? $state->datetime : null);
401
		if($oldDatetime) {
402
			$newDatetime->setTime($oldDatetime->format('H'), $oldDatetime->format('i'), $oldDatetime->format('s'));
403
		}
404
		$state->datetime = $newDatetime->format('Y-m-d H:i:s');
405
		$this->testSessionEnvironment->applyState($state);
406
	}
407
408
	/**
409
	 * Sets the current time. Relies on the underlying functionality using
410
	 * {@link \SS_Datetime::now()} rather than PHP's system time methods like date().
411
	 * Supports ISO fomat: H:i:s
412
	 * Example: Given the current time is "20:31:50"
413
	 *
414
	 * @Given /^the current time is "([^"]*)"$/
415
	 */
416 View Code Duplication
	public function givenTheCurrentTimeIs($time) {
0 ignored issues
show
Unused Code introduced by
The parameter $time is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
417
		$newDatetime = \DateTime::createFromFormat('H:i:s', $date);
0 ignored issues
show
Bug introduced by
The variable $date does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
418
		if(!$newDatetime) {
419
			throw new InvalidArgumentException(sprintf('Invalid date format: %s (requires "H:i:s")', $date));
420
		}
421
422
		$state = $this->testSessionEnvironment->getState();
423
		$oldDatetime = \DateTime::createFromFormat('Y-m-d H:i:s', isset($state->datetime) ? $state->datetime : null);
424
		if($oldDatetime) {
425
			$newDatetime->setDate($oldDatetime->format('Y'), $oldDatetime->format('m'), $oldDatetime->format('d'));
426
		}
427
		$state->datetime = $newDatetime->format('Y-m-d H:i:s');
428
		$this->testSessionEnvironment->applyState($state);
429
	}
430
431
	/**
432
	 * Selects option in select field with specified id|name|label|value.
433
	 *
434
	 * @override /^(?:|I )select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
435
	 */
436
	public function selectOption($select, $option) {
437
		// Find field
438
		$field = $this
439
			->getSession()
440
			->getPage()
441
			->findField($this->fixStepArgument($select));
442
443
		// If field is visible then select it as per normal
444
		if($field && $field->isVisible()) {
445
			parent::selectOption($select, $option);
446
		} else {
447
			$this->selectOptionWithJavascript($select, $option);
448
		}
449
	}
450
451
	/**
452
	 * Selects option in select field with specified id|name|label|value using javascript
453
	 * This method uses javascript to allow selection of options that may be
454
	 * overridden by javascript libraries, and thus hide the element.
455
	 *
456
	 * @When /^(?:|I )select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)" with javascript$/
457
	 */
458
	public function selectOptionWithJavascript($select, $option) {
459
		$select = $this->fixStepArgument($select);
460
		$option = $this->fixStepArgument($option);
461
		$page = $this->getSession()->getPage();
462
463
		// Find field
464
		$field = $page->findField($select);
465
		if (null === $field) {
466
			throw new ElementNotFoundException($this->getSession(), 'form field', 'id|name|label|value', $select);
467
		}
468
469
		// Find option
470
		$opt = $field->find('named', array(
471
			'option', $this->getSession()->getSelectorsHandler()->xpathLiteral($option)
472
		));
473
		if (null === $opt) {
474
			throw new ElementNotFoundException($this->getSession(), 'select option', 'value|text', $option);
475
		}
476
477
		// Merge new option in with old handling both multiselect and single select
478
		$value = $field->getValue();
479
		$newValue = $opt->getAttribute('value');
480
		if(is_array($value)) {
481
			if(!in_array($newValue, $value)) $value[] = $newValue;
482
		} else {
483
			$value = $newValue;
484
		}
485
		$valueEncoded = json_encode($value);
486
487
		// Inject this value via javascript
488
		$fieldID = $field->getAttribute('ID');
489
		$script = <<<EOS
490
			(function($) {
491
				$("#$fieldID")
492
					.val($valueEncoded)
493
					.change()
494
					.trigger('liszt:updated')
495
					.trigger('chosen:updated');
496
			})(jQuery);
497
EOS;
498
		$this->getSession()->getDriver()->executeScript($script);
499
	}
500
	
501
	
502
	/**
503
	 * @Override "When /^(?:|I )go to "(?P<page>[^"]+)"$/"
504
	 * We override this function to detect issues with .htaccess external redirects.
505
	 *
506
	 * For instance, if the behat test is being run with a base_url which includes a
507
	 * path, e.g. "http://localhost/behat-test-abc123/", .htaccess redirects may take the browser
508
	 * to the wrong base path, e.g. "http://localhost/", which will then probably generate
509
	 * a apache 404 response, which is pretty standard and we can detect it and give a better
510
	 * error message.
511
	 */
512
	public function visit($page){
513
		parent::visit($page);
514
			
515
		// We now check the response body. We would check for the response status code,
516
		// but that is not quite possible yet, so this is the best we can do.
517
		$page = $this->getSession()->getPage();
518
		$title = $page->find('css', 'title')->getHtml(); // getText returns empty string, so have to use getHtml
519
		assertNotEquals('404 Not Found', $title, 'A 404 response was detected from the server. If you intended to test an apache 404 response, please write a specific 404 test step.');
520
	}
521
	
522
}
523