Completed
Push — master ( 987505...ce3c0b )
by Hamish
8s
created

BehatExtension/Context/SilverStripeContext.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
24
25
/**
26
 * SilverStripeContext
27
 *
28
 * Generic context wrapper used as a base for Behat FeatureContext.
29
 */
30
class SilverStripeContext extends MinkContext implements SilverStripeAwareContextInterface
31
{
32
	protected $databaseName;
33
34
	/**
35
	 * @var Array Partial string match for step names
36
	 * that are considered to trigger Ajax request in the CMS,
37
	 * and hence need special timeout handling.
38
	 * @see \SilverStripe\BehatExtension\Context\BasicContext->handleAjaxBeforeStep().
39
	 */
40
	protected $ajaxSteps;
41
42
	/**
43
	 * @var Int Timeout in milliseconds, after which the interface assumes
44
	 * that an Ajax request has timed out, and continues with assertions.
45
	 */
46
	protected $ajaxTimeout;
47
48
	/**
49
	 * @var String Relative URL to the SilverStripe admin interface.
50
	 */
51
	protected $adminUrl;
52
53
	/**
54
	 * @var String Relative URL to the SilverStripe login form.
55
	 */
56
	protected $loginUrl;
57
58
	/**
59
	 * @var String Relative path to a writeable folder where screenshots can be stored.
60
	 * If set to NULL, no screenshots will be stored.
61
	 */
62
	protected $screenshotPath;
63
64
	protected $context;
65
66
	protected $testSessionEnvironment;
67
68
69
	/**
70
	 * Initializes context.
71
	 * Every scenario gets it's own context object.
72
	 *
73
	 * @param   array   $parameters     context parameters (set them up through behat.yml)
74
	 */
75
	public function __construct(array $parameters) {
76
		// Initialize your context here
77
		$this->context = $parameters;
78
		$this->testSessionEnvironment = new \TestSessionEnvironment();
79
	}
80
81
	public function setDatabase($databaseName) {
82
		$this->databaseName = $databaseName;
83
	}
84
85
	public function setAjaxSteps($ajaxSteps) {
86
		if($ajaxSteps) $this->ajaxSteps = $ajaxSteps;
87
	}
88
89
	public function getAjaxSteps() {
90
		return $this->ajaxSteps;
91
	}
92
93
	public function setAjaxTimeout($ajaxTimeout) {
94
		$this->ajaxTimeout = $ajaxTimeout;
95
	}
96
97
	public function getAjaxTimeout() {
98
		return $this->ajaxTimeout;
99
	}
100
101
	public function setAdminUrl($adminUrl) {
102
		$this->adminUrl = $adminUrl;
103
	}
104
105
	public function getAdminUrl() {
106
		return $this->adminUrl;
107
	}
108
109
	public function setLoginUrl($loginUrl) {
110
		$this->loginUrl = $loginUrl;
111
	}
112
113
	public function getLoginUrl() {
114
		return $this->loginUrl;
115
	}
116
117
	public function setScreenshotPath($screenshotPath) {
118
		$this->screenshotPath = $screenshotPath;
119
	}
120
121
	public function getScreenshotPath() {
122
		return $this->screenshotPath;
123
	}
124
125
	public function getRegionMap(){
126
		return $this->regionMap;
127
	}
128
129
	public function setRegionMap($regionMap){
130
		$this->regionMap = $regionMap;
131
	}
132
133
	/**
134
	 * Returns MinkElement based off region defined in .yml file.
135
	 * Also supports direct CSS selectors and regions identified by a "data-title" attribute.
136
	 * When using the "data-title" attribute, ensure not to include double quotes.
137
	 *
138
	 * @param String $region Region name or CSS selector
139
	 * @return MinkElement|null
140
	 */
141
	public function getRegionObj($region) {
142
		// Try to find regions directly by CSS selector.
143
		try {
144
			$regionObj = $this->getSession()->getPage()->find(
145
				'css',
146
				// Escape CSS selector
147
				(false !== strpos($region, "'")) ? str_replace("'", "\'", $region) : $region
148
			);
149
			if($regionObj) {
150
				return $regionObj;
151
			}
152
		} catch(\Symfony\Component\CssSelector\Exception\SyntaxErrorException $e) {
153
			// fall through to next case
154
		}
155
156
		// Fall back to region identified by data-title.
157
		// Only apply if no double quotes exist in search string,
158
		// which would break the CSS selector.
159
		if(false === strpos($region, '"')) {
160
			$regionObj = $this->getSession()->getPage()->find(
161
				'css',
162
				'[data-title="' . $region . '"]'
163
			);
164
			if($regionObj) {
165
				return $regionObj;
166
			}
167
		}
168
169
		// Look for named region
170
		if(!$this->regionMap) {
171
			throw new \LogicException("Cannot find 'region_map' in the behat.yml");
172
		}
173
		if(!array_key_exists($region, $this->regionMap)) {
174
			throw new \LogicException("Cannot find the specified region in the behat.yml");
175
		}
176
		$regionObj = $this->getSession()->getPage()->find('css', $region);
177
		if(!$regionObj) {
178
			throw new ElementNotFoundException("Cannot find the specified region on the page");
179
		}
180
181
		return $regionObj;
182
	}
183
184
	/**
185
	 * @BeforeScenario
186
	 */
187
	public function before(ScenarioEvent $event) {
188
		if (!isset($this->databaseName)) {
189
			throw new \LogicException(
190
				'Context\'s $databaseName has to be set when implementing SilverStripeAwareContextInterface.'
191
			);
192
		}
193
194
		$state = $this->getTestSessionState();
195
		$this->testSessionEnvironment->startTestSession($state);
196
197
		// Optionally import database
198
		if(!empty($state['importDatabasePath'])) {
199
			$this->testSessionEnvironment->importDatabase(
200
				$state['importDatabasePath'],
201
				!empty($state['requireDefaultRecords']) ? $state['requireDefaultRecords'] : false
202
			);
203
		} else if(!empty($state['requireDefaultRecords']) && $state['requireDefaultRecords']) {
204
			$this->testSessionEnvironment->requireDefaultRecords();
205
		}
206
207
		// Fixtures
208
		$fixtureFile = (!empty($state['fixture'])) ? $state['fixture'] : null;
209
		if($fixtureFile) {
210
			$this->testSessionEnvironment->loadFixtureIntoDb($fixtureFile);
211
		}
212
213
		if($screenSize = getenv('BEHAT_SCREEN_SIZE')) {
214
			list($screenWidth, $screenHeight) = explode('x', $screenSize);
215
			$this->getSession()->resizeWindow((int)$screenWidth, (int)$screenHeight);
216
		} else {
217
			$this->getSession()->resizeWindow(1024, 768);
218
		}
219
	}
220
221
	/**
222
	 * Returns a parameter map of state to set within the test session.
223
	 * Takes TESTSESSION_PARAMS environment variable into account for run-specific configurations.
224
	 *
225
	 * @return array
226
	 */
227
	public function getTestSessionState() {
228
		$extraParams = array();
229
		parse_str(getenv('TESTSESSION_PARAMS'), $extraParams);
230
		return array_merge(
231
			array(
232
				'database' => $this->databaseName,
233
				'mailer' => 'SilverStripe\BehatExtension\Utility\TestMailer',
234
			),
235
			$extraParams
236
		);
237
	}
238
239
	/**
240
	 * Parses given URL and returns its components
241
	 *
242
	 * @param $url
243
	 * @return array|mixed Parsed URL
244
	 */
245
	public function parseUrl($url) {
246
		$url = parse_url($url);
247
		$url['vars'] = array();
248
		if (!isset($url['fragment'])) {
249
			$url['fragment'] = null;
250
		}
251
		if (isset($url['query'])) {
252
			parse_str($url['query'], $url['vars']);
253
		}
254
255
		return $url;
256
	}
257
258
	/**
259
	 * Checks whether current URL is close enough to the given URL.
260
	 * Unless specified in $url, get vars will be ignored
261
	 * Unless specified in $url, fragment identifiers will be ignored
262
	 *
263
	 * @param $url string URL to compare to current URL
264
	 * @return boolean Returns true if the current URL is close enough to the given URL, false otherwise.
265
	 */
266
	public function isCurrentUrlSimilarTo($url) {
267
		$current = $this->parseUrl($this->getSession()->getCurrentUrl());
268
		$test = $this->parseUrl($url);
269
270
		if ($current['path'] !== $test['path']) {
271
			return false;
272
		}
273
274
		if (isset($test['fragment']) && $current['fragment'] !== $test['fragment']) {
275
			return false;
276
		}
277
278
		foreach ($test['vars'] as $name => $value) {
279
			if (!isset($current['vars'][$name]) || $current['vars'][$name] !== $value) {
280
				return false;
281
			}
282
		}
283
284
		return true;
285
	}
286
287
	/**
288
	 * Returns base URL parameter set in MinkExtension.
289
	 * It simplifies configuration by allowing to specify this parameter
290
	 * once but makes code dependent on MinkExtension.
291
	 *
292
	 * @return string
293
	 */
294
	public function getBaseUrl() {
295
		return $this->getMinkParameter('base_url') ?: '';
296
	}
297
298
	/**
299
	 * Joins URL parts into an URL using forward slash.
300
	 * Forward slash usages are normalised to one between parts.
301
	 * This method takes variable number of parameters.
302
	 *
303
	 * @param $...
304
	 * @return string
305
	 * @throws \InvalidArgumentException
306
	 */
307
	public function joinUrlParts() {
308
		if (0 === func_num_args()) {
309
			throw new \InvalidArgumentException('Need at least one argument');
310
		}
311
312
		$parts = func_get_args();
313
		$trimSlashes = function(&$part) {
314
			$part = trim($part, '/');
315
		};
316
		array_walk($parts, $trimSlashes);
317
318
		return implode('/', $parts);
319
	}
320
321
	public function canIntercept() {
322
		$driver = $this->getSession()->getDriver();
323
		if ($driver instanceof GoutteDriver) {
324
			return true;
325
		}
326
		else {
327
			if ($driver instanceof Selenium2Driver) {
328
				return false;
329
			}
330
		}
331
332
		throw new UnsupportedDriverActionException('You need to tag the scenario with "@mink:goutte" or
333
			"@mink:symfony". Intercepting the redirections is not supported by %s', $driver);
334
	}
335
336
	/**
337
	 * @Given /^(.*) without redirection$/
338
	 */
339
	public function theRedirectionsAreIntercepted($step) {
340
		if ($this->canIntercept()) {
341
			$this->getSession()->getDriver()->getClient()->followRedirects(false);
342
		}
343
344
		return new Step\Given($step);
345
	}
346
347
	/**
348
	 * Fills in form field with specified id|name|label|value.
349
	 * Overwritten to select the first *visible* element, see https://github.com/Behat/Mink/issues/311
350
	 */
351 View Code Duplication
	public function fillField($field, $value) {
352
		$value = $this->fixStepArgument($value);
353
		$fields = $this->getSession()->getPage()->findAll('named', array(
354
			'field', $this->getSession()->getSelectorsHandler()->xpathLiteral($field)
355
		));
356
		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...
357
			if($f->isVisible()) {
358
				$f->setValue($value);
359
				return;
360
			}
361
		}
362
363
		throw new ElementNotFoundException(
364
			$this->getSession(), 'form field', 'id|name|label|value', $field
365
		);
366
	}
367
368
	/**
369
	 * Overwritten to click the first *visable* link the DOM.
370
	 */
371 View Code Duplication
	public function clickLink($link) {
372
		$link = $this->fixStepArgument($link);
373
		$links = $this->getSession()->getPage()->findAll('named', array(
374
			'link', $this->getSession()->getSelectorsHandler()->xpathLiteral($link)
375
		));
376
		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...
377
			if($l->isVisible()) {
378
				$l->click();
379
				return;
380
			}
381
		}
382
		throw new ElementNotFoundException(
383
			$this->getSession(), 'link', 'id|name|label|value', $link
384
		);
385
	}
386
387
	 /**
388
	 * Sets the current date. Relies on the underlying functionality using
389
	 * {@link SS_Datetime::now()} rather than PHP's system time methods like date().
390
	 * Supports ISO fomat: Y-m-d
391
	 * Example: Given the current date is "2009-10-31"
392
	 *
393
	 * @Given /^the current date is "([^"]*)"$/
394
	 */
395 View Code Duplication
	public function givenTheCurrentDateIs($date) {
396
		$newDatetime = \DateTime::createFromFormat('Y-m-d', $date);
397
		if(!$newDatetime) {
398
			throw new InvalidArgumentException(sprintf('Invalid date format: %s (requires "Y-m-d")', $date));
399
		}
400
401
		$state = $this->testSessionEnvironment->getState();
402
		$oldDatetime = \DateTime::createFromFormat('Y-m-d H:i:s', isset($state->datetime) ? $state->datetime : null);
403
		if($oldDatetime) {
404
			$newDatetime->setTime($oldDatetime->format('H'), $oldDatetime->format('i'), $oldDatetime->format('s'));
405
		}
406
		$state->datetime = $newDatetime->format('Y-m-d H:i:s');
407
		$this->testSessionEnvironment->applyState($state);
408
	}
409
410
	/**
411
	 * Sets the current time. Relies on the underlying functionality using
412
	 * {@link \SS_Datetime::now()} rather than PHP's system time methods like date().
413
	 * Supports ISO fomat: H:i:s
414
	 * Example: Given the current time is "20:31:50"
415
	 *
416
	 * @Given /^the current time is "([^"]*)"$/
417
	 */
418 View Code Duplication
	public function givenTheCurrentTimeIs($time) {
419
		$newDatetime = \DateTime::createFromFormat('H:i:s', $date);
420
		if(!$newDatetime) {
421
			throw new InvalidArgumentException(sprintf('Invalid date format: %s (requires "H:i:s")', $date));
422
		}
423
424
		$state = $this->testSessionEnvironment->getState();
425
		$oldDatetime = \DateTime::createFromFormat('Y-m-d H:i:s', isset($state->datetime) ? $state->datetime : null);
426
		if($oldDatetime) {
427
			$newDatetime->setDate($oldDatetime->format('Y'), $oldDatetime->format('m'), $oldDatetime->format('d'));
428
		}
429
		$state->datetime = $newDatetime->format('Y-m-d H:i:s');
430
		$this->testSessionEnvironment->applyState($state);
431
	}
432
433
	/**
434
	 * Selects option in select field with specified id|name|label|value.
435
	 *
436
	 * @override /^(?:|I )select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
437
	 */
438
	public function selectOption($select, $option) {
439
		// Find field
440
		$field = $this
441
			->getSession()
442
			->getPage()
443
			->findField($this->fixStepArgument($select));
444
445
		// If field is visible then select it as per normal
446
		if($field && $field->isVisible()) {
447
			parent::selectOption($select, $option);
448
		} else {
449
			$this->selectOptionWithJavascript($select, $option);
450
		}
451
	}
452
453
	/**
454
	 * Selects option in select field with specified id|name|label|value using javascript
455
	 * This method uses javascript to allow selection of options that may be
456
	 * overridden by javascript libraries, and thus hide the element.
457
	 *
458
	 * @When /^(?:|I )select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)" with javascript$/
459
	 */
460
	public function selectOptionWithJavascript($select, $option) {
461
		$select = $this->fixStepArgument($select);
462
		$option = $this->fixStepArgument($option);
463
		$page = $this->getSession()->getPage();
464
465
		// Find field
466
		$field = $page->findField($select);
467
		if (null === $field) {
468
			throw new ElementNotFoundException($this->getSession(), 'form field', 'id|name|label|value', $select);
469
		}
470
471
		// Find option
472
		$opt = $field->find('named', array(
473
			'option', $this->getSession()->getSelectorsHandler()->xpathLiteral($option)
474
		));
475
		if (null === $opt) {
476
			throw new ElementNotFoundException($this->getSession(), 'select option', 'value|text', $option);
477
		}
478
479
		// Merge new option in with old handling both multiselect and single select
480
		$value = $field->getValue();
481
		$newValue = $opt->getAttribute('value');
482
		if(is_array($value)) {
483
			if(!in_array($newValue, $value)) $value[] = $newValue;
484
		} else {
485
			$value = $newValue;
486
		}
487
		$valueEncoded = json_encode($value);
488
489
		// Inject this value via javascript
490
		$fieldID = $field->getAttribute('ID');
491
		$script = <<<EOS
492
			(function($) {
493
				$("#$fieldID")
494
					.val($valueEncoded)
495
					.change()
496
					.trigger('liszt:updated')
497
					.trigger('chosen:updated');
498
			})(jQuery);
499
EOS;
500
		$this->getSession()->getDriver()->executeScript($script);
501
	}
502
503
}
504