Completed
Push — master ( 420f16...68b944 )
by Alexander
03:24
created

BrowserConfiguration   B

Complexity

Total Complexity 54

Size/Duplication

Total Lines 586
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 97.81%

Importance

Changes 20
Bugs 0 Features 2
Metric Value
wmc 54
c 20
b 0
f 2
lcom 1
cbo 7
dl 0
loc 586
ccs 134
cts 137
cp 0.9781
rs 7.0642

30 Methods

Rating   Name   Duplication   Size   Complexity  
A getType() 0 4 1
A prepareParameters() 0 4 1
A setDriverOptions() 0 4 1
A setDesiredCapabilities() 0 4 1
A setSessionStrategy() 0 4 1
A isShared() 0 4 1
A resolveAliases() 0 17 3
A __construct() 0 11 2
A getSubscribedEvents() 0 7 1
A attachToTestCase() 0 7 1
A detachFromTestCase() 0 5 1
A getTestCase() 0 8 2
A setAliases() 0 6 1
B setup() 0 22 4
A setDriver() 0 11 2
A setHost() 0 8 2
A setPort() 0 8 2
A setBrowserName() 0 8 2
A setBaseUrl() 0 8 2
A setTimeout() 0 8 2
A setParameter() 0 10 2
A getParameter() 0 10 2
A createDriver() 0 6 1
A getSessionStrategyHash() 0 10 2
A getTestStatus() 0 10 2
A getChecksum() 0 6 1
B arrayMergeRecursive() 0 17 5
A __call() 0 10 2
A onTestSetup() 0 8 2
A onTestEnded() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like BrowserConfiguration often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BrowserConfiguration, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the phpunit-mink library.
4
 * For the full copyright and license information, please view
5
 * the LICENSE file that was distributed with this source code.
6
 *
7
 * @copyright Alexander Obuhovich <[email protected]>
8
 * @link      https://github.com/aik099/phpunit-mink
9
 */
10
11
namespace aik099\PHPUnit\BrowserConfiguration;
12
13
14
use aik099\PHPUnit\BrowserTestCase;
15
use aik099\PHPUnit\Event\TestEndedEvent;
16
use aik099\PHPUnit\Event\TestEvent;
17
use aik099\PHPUnit\MinkDriver\DriverFactoryRegistry;
18
use aik099\PHPUnit\MinkDriver\IMinkDriverFactory;
19
use aik099\PHPUnit\Session\ISessionStrategyFactory;
20
use Behat\Mink\Driver\DriverInterface;
21
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
22
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
23
24
/**
25
 * Browser configuration for browser.
26
 *
27
 * @method \Mockery\Expectation shouldReceive(string $name)
28
 * @method string getDriver() Returns Mink driver name.
29
 * @method array getDriverOptions() Returns Mink driver options.
30
 * @method string getHost() Returns hostname from browser configuration.
31
 * @method integer getPort() Returns port from browser configuration.
32
 * @method string getBrowserName() Returns browser name from browser configuration.
33
 * @method string getBaseUrl() Returns default browser url from browser configuration.
34
 * @method array getDesiredCapabilities() Returns desired capabilities from browser configuration.
35
 * @method integer getTimeout() Returns server timeout.
36
 * @method string getSessionStrategy() Returns session strategy name.
37
 */
38
class BrowserConfiguration implements EventSubscriberInterface
39
{
40
	const TYPE = 'default';
41
42
	/**
43
	 * User defaults.
44
	 *
45
	 * @var array
46
	 */
47
	protected $defaults = array(
48
		// Driver related.
49
		'host' => 'localhost',
50
		'driver' => 'selenium2',
51
		'driverOptions' => array(),
52
53
		// TODO: Move under 'driverOptions' of 'selenium2' driver (BC break).
54
		'desiredCapabilities' => array(),
55
		'timeout' => 60,
56
57
		// Browser related.
58
		'browserName' => 'firefox', // Have no effect on headless drivers.
59
		'baseUrl' => '',
60
61
		// Test related.
62
		'sessionStrategy' => ISessionStrategyFactory::TYPE_ISOLATED,
63
	);
64
65
	/**
66
	 * User defaults merged with driver defaults.
67
	 *
68
	 * @var array
69
	 */
70
	private $_mergedDefaults = array();
71
72
	/**
73
	 * Manually set browser configuration parameters.
74
	 *
75
	 * @var array
76
	 */
77
	private $_parameters = array();
78
79
	/**
80
	 * Browser configuration aliases.
81
	 *
82
	 * @var array
83
	 */
84
	protected $aliases;
85
86
	/**
87
	 * Test case.
88
	 *
89
	 * @var BrowserTestCase
90
	 */
91
	private $_testCase;
92
93
	/**
94
	 * Event dispatcher.
95
	 *
96
	 * @var EventDispatcherInterface
97
	 */
98
	private $_eventDispatcher;
99
100
	/**
101
	 * Driver factory registry.
102
	 *
103
	 * @var DriverFactoryRegistry
104
	 */
105
	private $_driverFactoryRegistry;
106
107
	/**
108
	 * Driver factory.
109
	 *
110
	 * @var IMinkDriverFactory
111
	 */
112
	private $_driverFactory;
113
114
	/**
115
	 * Resolves browser alias into corresponding browser configuration.
116
	 *
117
	 * @param array $parameters Browser configuration.
118
	 * @param array $aliases    Browser configuration aliases.
119
	 *
120
	 * @return array
121
	 * @throws \InvalidArgumentException When unable to resolve used browser alias.
122
	 */
123 34
	public static function resolveAliases(array $parameters, array $aliases)
124
	{
125 34
		if ( !isset($parameters['alias']) ) {
126 31
			return $parameters;
127
		}
128
129 16
		$browser_alias = $parameters['alias'];
130 16
		unset($parameters['alias']);
131
132 16
		if ( isset($aliases[$browser_alias]) ) {
133 13
			$candidate_params = self::arrayMergeRecursive($aliases[$browser_alias], $parameters);
134
135 13
			return self::resolveAliases($candidate_params, $aliases);
136
		}
137
138 3
		throw new \InvalidArgumentException(sprintf('Unable to resolve "%s" browser alias', $browser_alias));
139
	}
140
141
	/**
142
	 * Creates browser configuration.
143
	 *
144
	 * @param EventDispatcherInterface $event_dispatcher        Event dispatcher.
145
	 * @param DriverFactoryRegistry    $driver_factory_registry Driver factory registry.
146
	 */
147 156
	public function __construct(
148
		EventDispatcherInterface $event_dispatcher,
149
		DriverFactoryRegistry $driver_factory_registry
150
	) {
151 156
		$this->_eventDispatcher = $event_dispatcher;
152 156
		$this->_driverFactoryRegistry = $driver_factory_registry;
153
154 156
		if ( $this->defaults['driver'] ) {
155 156
			$this->setDriver($this->defaults['driver']);
156 156
		}
157 156
	}
158
159
	/**
160
	 * Returns type of browser configuration.
161
	 *
162
	 * @return string
163
	 */
164 8
	public function getType()
165
	{
166 8
		return static::TYPE;
167
	}
168
169
	/**
170
	 * Returns an array of event names this subscriber wants to listen to.
171
	 *
172
	 * @return array The event names to listen to
173
	 */
174 27
	public static function getSubscribedEvents()
175
	{
176
		return array(
177 27
			BrowserTestCase::TEST_SETUP_EVENT => array('onTestSetup', 100),
178 27
			BrowserTestCase::TEST_ENDED_EVENT => array('onTestEnded', 100),
179 27
		);
180
	}
181
182
	/**
183
	 * Attaches listeners.
184
	 *
185
	 * @param BrowserTestCase $test_case Test case.
186
	 *
187
	 * @return self
188
	 */
189 40
	public function attachToTestCase(BrowserTestCase $test_case)
190
	{
191 40
		$this->_testCase = $test_case;
192 40
		$this->_eventDispatcher->addSubscriber($this);
193
194 40
		return $this;
195
	}
196
197
	/**
198
	 * Detaches listeners.
199
	 *
200
	 * @return void
201
	 */
202 10
	protected function detachFromTestCase()
203
	{
204 10
		$this->_testCase = null;
205 10
		$this->_eventDispatcher->removeSubscriber($this);
206 10
	}
207
208
	/**
209
	 * Returns associated test case.
210
	 *
211
	 * @return BrowserTestCase
212
	 * @throws \RuntimeException When test case not attached.
213
	 */
214 44
	public function getTestCase()
215
	{
216 44
		if ( $this->_testCase === null ) {
217 3
			throw new \RuntimeException('Test Case not attached, use "attachToTestCase" method');
218
		}
219
220 41
		return $this->_testCase;
221
	}
222
223
	/**
224
	 * Sets aliases.
225
	 *
226
	 * @param array $aliases Browser configuration aliases.
227
	 *
228
	 * @return self
229
	 */
230 148
	public function setAliases(array $aliases = array())
231
	{
232 148
		$this->aliases = $aliases;
233
234 148
		return $this;
235
	}
236
237
	/**
238
	 * Initializes a browser with given configuration.
239
	 *
240
	 * @param array $parameters Browser configuration parameters.
241
	 *
242
	 * @return self
243
	 * @throws \InvalidArgumentException When unknown parameter is discovered.
244
	 */
245 31
	public function setup(array $parameters)
246
	{
247 31
		$parameters = $this->prepareParameters($parameters);
248
249
		// Make sure, that 'driver' parameter is handled first.
250 28
		if ( isset($parameters['driver']) ) {
251 28
			$this->setDriver($parameters['driver']);
252 28
			unset($parameters['driver']);
253 28
		}
254
255 28
		foreach ( $parameters as $name => $value ) {
256 25
			$method = 'set' . ucfirst($name);
257
258 25
			if ( !method_exists($this, $method) ) {
259 3
				throw new \InvalidArgumentException('Unable to set unknown parameter "' . $name . '"');
260
			}
261
262 22
			$this->$method($value);
263 25
		}
264
265 25
		return $this;
266
	}
267
268
	/**
269
	 * Merges together default, given parameter and resolves aliases along the way.
270
	 *
271
	 * @param array $parameters Browser configuration parameters.
272
	 *
273
	 * @return array
274
	 */
275 31
	protected function prepareParameters(array $parameters)
276
	{
277 31
		return array_merge($this->_parameters, self::resolveAliases($parameters, $this->aliases));
278
	}
279
280
	/**
281
	 * Sets Mink driver to browser configuration.
282
	 *
283
	 * @param string $driver_name Mink driver name.
284
	 *
285
	 * @return self
286
	 * @throws \InvalidArgumentException When Mink driver name is not a string.
287
	 */
288 157
	public function setDriver($driver_name)
289
	{
290 157
		if ( !is_string($driver_name) ) {
291 3
			throw new \InvalidArgumentException('The Mink driver name must be a string');
292
		}
293
294 157
		$this->_driverFactory = $this->_driverFactoryRegistry->get($driver_name);
295 157
		$this->_mergedDefaults = self::arrayMergeRecursive($this->defaults, $this->_driverFactory->getDriverDefaults());
296
297 157
		return $this->setParameter('driver', $driver_name);
298
	}
299
300
	/**
301
	 * Sets Mink driver options to browser configuration.
302
	 *
303
	 * @param array $driver_options Mink driver options.
304
	 *
305
	 * @return self
306
	 */
307 6
	public function setDriverOptions(array $driver_options)
308
	{
309 6
		return $this->setParameter('driverOptions', $driver_options);
310
	}
311
312
	/**
313
	 * Sets hostname to browser configuration.
314
	 *
315
	 * To be called from TestCase::setUp().
316
	 *
317
	 * @param string $host Hostname.
318
	 *
319
	 * @return self
320
	 * @throws \InvalidArgumentException When host is not a string.
321
	 */
322 25
	public function setHost($host)
323
	{
324 25
		if ( !is_string($host) ) {
325 3
			throw new \InvalidArgumentException('Host must be a string');
326
		}
327
328 22
		return $this->setParameter('host', $host);
329
	}
330
331
	/**
332
	 * Sets port to browser configuration.
333
	 *
334
	 * To be called from TestCase::setUp().
335
	 *
336
	 * @param integer $port Port.
337
	 *
338
	 * @return self
339
	 * @throws \InvalidArgumentException When port isn't a number.
340
	 */
341 24
	public function setPort($port)
342
	{
343 24
		if ( !is_int($port) ) {
344 3
			throw new \InvalidArgumentException('Port must be an integer');
345
		}
346
347 21
		return $this->setParameter('port', $port);
348
	}
349
350
	/**
351
	 * Sets browser name to browser configuration.
352
	 *
353
	 * To be called from TestCase::setUp().
354
	 *
355
	 * @param string $browser_name Browser name.
356
	 *
357
	 * @return self
358
	 * @throws \InvalidArgumentException When browser name isn't a string.
359
	 */
360 26
	public function setBrowserName($browser_name)
361
	{
362 26
		if ( !is_string($browser_name) ) {
363 3
			throw new \InvalidArgumentException('Browser must be a string');
364
		}
365
366 23
		return $this->setParameter('browserName', $browser_name);
367
	}
368
369
	/**
370
	 * Sets default browser url to browser configuration.
371
	 *
372
	 * To be called from TestCase::setUp().
373
	 *
374
	 * @param string $base_url Default browser url.
375
	 *
376
	 * @return self
377
	 * @throws \InvalidArgumentException When browser url isn't a string.
378
	 */
379 13
	public function setBaseUrl($base_url)
380
	{
381 13
		if ( !is_string($base_url) ) {
382 3
			throw new \InvalidArgumentException('Base url must be a string');
383
		}
384
385 10
		return $this->setParameter('baseUrl', $base_url);
386
	}
387
388
	/**
389
	 * Sets desired capabilities to browser configuration.
390
	 *
391
	 * To be called from TestCase::setUp().
392
	 *
393
	 * @param array $capabilities Desired capabilities.
394
	 *
395
	 * @return self
396
	 * @link   http://code.google.com/p/selenium/wiki/JsonWireProtocol
397
	 */
398 32
	public function setDesiredCapabilities(array $capabilities)
399
	{
400 32
		return $this->setParameter('desiredCapabilities', $capabilities);
401
	}
402
403
	/**
404
	 * Sets server timeout.
405
	 * To be called from TestCase::setUp().
406
	 *
407
	 * @param integer $timeout Server timeout in seconds.
408
	 *
409
	 * @return self
410
	 * @throws \InvalidArgumentException When timeout isn't integer.
411
	 */
412 9
	public function setTimeout($timeout)
413
	{
414 9
		if ( !is_int($timeout) ) {
415 3
			throw new \InvalidArgumentException('Timeout must be an integer');
416
		}
417
418 6
		return $this->setParameter('timeout', $timeout);
419
	}
420
421
	/**
422
	 * Sets session strategy name.
423
	 *
424
	 * @param string $session_strategy Session strategy name.
425
	 *
426
	 * @return self
427
	 */
428 40
	public function setSessionStrategy($session_strategy)
429
	{
430 40
		return $this->setParameter('sessionStrategy', $session_strategy);
431
	}
432
433
	/**
434
	 * Tells if browser configuration requires a session, that is shared across tests in a test case.
435
	 *
436
	 * @return boolean
437
	 */
438 42
	public function isShared()
439
	{
440 42
		return $this->getSessionStrategy() == ISessionStrategyFactory::TYPE_SHARED;
441
	}
442
443
	/**
444
	 * Sets parameter.
445
	 *
446
	 * @param string $name  Parameter name.
447
	 * @param mixed  $value Parameter value.
448
	 *
449
	 * @return self
450
	 * @throws \LogicException When driver wasn't set upfront.
451
	 */
452 163
	protected function setParameter($name, $value)
453
	{
454 163
		if ( !isset($this->_driverFactory) ) {
455
			throw new \LogicException('Please set "driver" parameter first.');
456
		}
457
458 163
		$this->_parameters[$name] = $value;
459
460 163
		return $this;
461
	}
462
463
	/**
464
	 * Returns parameter value.
465
	 *
466
	 * @param string $name Name.
467
	 *
468
	 * @return mixed
469
	 * @throws \InvalidArgumentException When unknown parameter was requested.
470
	 */
471 103
	protected function getParameter($name)
472
	{
473 103
		$merged = self::arrayMergeRecursive($this->_mergedDefaults, $this->_parameters);
474
475 103
		if ( array_key_exists($name, $merged) ) {
476 100
			return $merged[$name];
477
		}
478
479 3
		throw new \InvalidArgumentException('Unable to get unknown parameter "' . $name . '"');
480
	}
481
482
	/**
483
	 * Creates driver based on browser configuration.
484
	 *
485
	 * @return DriverInterface
486
	 */
487 7
	public function createDriver()
488
	{
489 7
		$factory = $this->_driverFactoryRegistry->get($this->getDriver());
490
491 7
		return $factory->createDriver($this);
492
	}
493
494
	/**
495
	 * Returns session strategy hash based on given test case and current browser configuration.
496
	 *
497
	 * @return string
498
	 */
499 12
	public function getSessionStrategyHash()
500
	{
501 12
		$ret = $this->getChecksum();
502
503 12
		if ( $this->isShared() ) {
504 6
			$ret .= '::' . get_class($this->getTestCase());
505 6
		}
506
507 12
		return $ret;
508
	}
509
510
	/**
511
	 * Returns test run status based on session strategy requested by browser.
512
	 *
513
	 * @param BrowserTestCase               $test_case   Browser test case.
514
	 * @param \PHPUnit_Framework_TestResult $test_result Test result.
515
	 *
516
	 * @return boolean
517
	 * @see    IsolatedSessionStrategy
518
	 * @see    SharedSessionStrategy
519
	 */
520 10
	public function getTestStatus(BrowserTestCase $test_case, \PHPUnit_Framework_TestResult $test_result)
521
	{
522 10
		if ( $this->isShared() ) {
523
			// All tests in a test case use same session -> failed even if 1 test fails.
524 3
			return $test_result->wasSuccessful();
525
		}
526
527
		// Each test in a test case are using it's own session -> failed if test fails.
528 7
		return !$test_case->hasFailed();
529
	}
530
531
	/**
532
	 * Returns checksum from current configuration.
533
	 *
534
	 * @return integer
535
	 */
536 44
	public function getChecksum()
537
	{
538 44
		ksort($this->_parameters);
539
540 44
		return crc32(serialize($this->_parameters));
541
	}
542
543
	/**
544
	 * Similar to array_merge_recursive but keyed-valued are always overwritten.
545
	 *
546
	 * Priority goes to the 2nd array.
547
	 *
548
	 * @param mixed $array1 First array.
549
	 * @param mixed $array2 Second array.
550
	 *
551
	 * @return array
552
	 */
553 163
	protected static function arrayMergeRecursive($array1, $array2)
554
	{
555 163
		if ( !is_array($array1) || !is_array($array2) ) {
556 155
			return $array2;
557
		}
558
559 163
		foreach ( $array2 as $array2_key => $array2_value ) {
560 162
			if ( isset($array1[$array2_key]) ) {
561 162
				$array1[$array2_key] = self::arrayMergeRecursive($array1[$array2_key], $array2_value);
562 162
			}
563
			else {
564 162
				$array1[$array2_key] = $array2_value;
565
			}
566 163
		}
567
568 163
		return $array1;
569
	}
570
571
	/**
572
	 * Allows to retrieve a parameter by name.
573
	 *
574
	 * @param string $method Method name.
575
	 * @param array  $args   Arguments.
576
	 *
577
	 * @return mixed
578
	 * @throws \BadMethodCallException When non-parameter getter method is invoked.
579
	 */
580 106
	public function __call($method, array $args)
581
	{
582 106
		if ( substr($method, 0, 3) === 'get' ) {
583 103
			return $this->getParameter(lcfirst(substr($method, 3)));
584
		}
585
586 3
		throw new \BadMethodCallException(
587 3
			'Method "' . $method . '" does not exist on ' . get_class($this) . ' class'
588 3
		);
589
	}
590
591
	/**
592
	 * Hook, called from "BrowserTestCase::setUp" method.
593
	 *
594
	 * @param TestEvent $event Test event.
595
	 *
596
	 * @return void
597
	 */
598 24
	public function onTestSetup(TestEvent $event)
599
	{
600 24
		if ( !$event->validateSubscriber($this->getTestCase()) ) {
601
			return;
602
		}
603
604
		// Place code here.
605 24
	}
606
607
	/**
608
	 * Hook, called from "BrowserTestCase::run" method.
609
	 *
610
	 * @param TestEndedEvent $event Test ended event.
611
	 *
612
	 * @return void
613
	 */
614 10
	public function onTestEnded(TestEndedEvent $event)
615
	{
616 10
		if ( !$event->validateSubscriber($this->getTestCase()) ) {
617
			return;
618
		}
619
620 10
		$this->detachFromTestCase();
621 10
	}
622
623
}
624