Completed
Push — master ( b459f9...8174b2 )
by Kevin
02:00
created

AbstractTestCase::endTimer()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 18
nc 2
nop 2
1
<?php
2
3
namespace Magium;
4
5
use Facebook\WebDriver\Exception\NoSuchElementException;
6
use Facebook\WebDriver\Exception\WebDriverException;
7
use Facebook\WebDriver\WebDriverElement;
8
use Magium\Assertions\Element\Clickable;
9
use Magium\Assertions\Element\Displayed;
10
use Magium\Assertions\Element\Exists;
11
use Magium\Assertions\Element\NotClickable;
12
use Magium\Assertions\Element\NotDisplayed;
13
use Magium\Assertions\Element\NotExists;
14
use Magium\Assertions\LoggingAssertionExecutor;
15
use Magium\Commands\Open;
16
use Magium\TestCase\Initializer;
17
use Magium\TestCase\InitializerContainer;
18
use Magium\Themes\BaseThemeInterface;
19
use Magium\Util\Log\LoggerInterface;
20
use Magium\Util\Phpunit\MasterListener;
21
use Magium\Util\Phpunit\MasterListenerInterface;
22
use Magium\WebDriver\WebDriver;
23
use PHPUnit\Framework\TestCase;
24
use Zend\Di\Di;
25
26
abstract class AbstractTestCase extends TestCase
27
{
28
29
    protected static $baseNamespaces = [];
30
31
    protected $baseThemeClass = 'Magium\Themes\ThemeConfigurationInterface';
32
33
    protected $postCallbacks = [];
34
35
    /**
36
     * @var MasterListener
37
     */
38
39
    protected static $masterListener;
40
41
    /**
42
     * @var \Magium\WebDriver\WebDriver
43
     */
44
    protected $webdriver;
45
46
    /**
47
     * @var Di
48
     */
49
50
    protected $di;
51
52
    protected $textElementNodeSearch = [
53
        'button', 'span', 'a', 'li', 'label', 'option', 'h1', 'h2', 'h3', 'td'
54
    ];
55
56
    protected $initializer;
57
58
    const BY_XPATH = 'byXpath';
59
    const BY_ID    = 'byId';
60
    const BY_CSS_SELECTOR = 'byCssSelector';
61
    const BY_TEXT = 'byText';
62
63
    private $sectionTimerStart;
64
65
    public function startTimer()
66
    {
67
        $this->sectionTimerStart = microtime(true);
68
    }
69
70
    public function endTimer($name, $minimumElapsedTime = 0)
71
    {
72
        $endTime = microtime(true);
73
        if (!$this->sectionTimerStart) {
74
            throw new \Exception('sendTimer(name) requires startTimer() to be called previously');
75
        }
76
77
        $elapsedMs = (int)(($endTime - $this->sectionTimerStart) * 1000);
78
        $elapsedExact = $endTime - $this->sectionTimerStart;
79
80
        $pass = $elapsedExact > $minimumElapsedTime;
81
82
        $message = sprintf('%s - elapsed %s', $name, $elapsedMs);
83
        $this->getLogger()->info($message, [
84
            'type' => 'timer',
85
            'name' => $name,
86
            'elapsed_ms' => $elapsedMs,
87
            'elapsed' => $elapsedExact,
88
            'start_time' => $this->sectionTimerStart,
89
            'end_time' => $endTime,
90
            'pass' => $pass
91
        ]);
92
        $this->sectionTimerStart = false;
93
        self::assertGreaterThan($minimumElapsedTime, $elapsedExact);
94
    }
95
96
    protected static $registrationCallbacks;
97
98
    protected function setUp()
99
    {
100
        /**
101
         * Putting this in the setup and not in the property means that an extending class can inject itself easily
102
         * before the Magium namespace, thus, taking preference over the base namespace
103
         */
104
105
        self::addBaseNamespace('Magium');
106
107
        /**
108
         * This weird little bit of code (the InitializerContainer) is done like this so we can provide a type
109
         * preference for the Initializer.  This is because Magium is a DI-based setup rigged inside a non-DI-based
110
         * setup
111
         */
112
        if (!$this->initializer instanceof InitializerContainer) {
113
            $this->initializer = Initializer::getInitializationDependencyInjectionContainer()->get(InitializerContainer::class);
114
        }
115
        $this->initializer->initialize($this);
116
    }
117
118
    /**
119
     * @return Di
120
     */
121
    public function getDi()
122
    {
123
        return $this->di;
124
    }
125
126
    /**
127
     * @param Di $di
128
     */
129
    public function setDi($di)
130
    {
131
        $this->di = $di;
132
    }
133
134
    /**
135
     * @return WebDriver
136
     */
137
    public function getWebdriver()
138
    {
139
        if (!$this->webdriver instanceof WebDriver) {
140
            $this->webdriver = $this->get(WebDriver::class);
141
        }
142
        return $this->webdriver;
143
    }
144
145
    /**
146
     * @param WebDriver $webdriver
147
     */
148
    public function setWebdriver(WebDriver $webdriver)
149
    {
150
        $this->webdriver = $webdriver;
151
    }
152
153
    public function getInitializer()
154
    {
155
        return $this->initializer;
156
    }
157
158
159
    public function __construct($name = null, array $data = [], $dataName = null, Initializer $initializer = null)
0 ignored issues
show
Unused Code introduced by
The parameter $initializer 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...
160
    {
161
        // Init at the earliest possible convenience.
162
        self::getMasterListener();
163
        parent::__construct($name, $data, $dataName);
164
    }
165
166
    public function setUseErrorHandlerFromAnnotation() {
167
        /*
168
         * This odd little function is here because the first place where you can reliably add a listener without
169
         * having to make a phpunit.xml or program argument change AND the result object is available AND there is
170
         * no incompatibility with PHPUnit 5.
171
         */
172
        $result = $this->getTestResultObject();
173
        self::getMasterListener()->bindToResult($result);
174
        return parent::setUseErrorHandlerFromAnnotation();
175
    }
176
177
    /**
178
     * @return MasterListener
179
     */
180
181
    public static function getMasterListener()
182
    {
183
        if (!self::$masterListener instanceof MasterListenerInterface) {
184
            if (interface_exists('PHPUnit_Framework_Test')) {
185
                self::$masterListener = new \Magium\Util\Phpunit5\MasterListener();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Magium\Util\Phpunit5\MasterListener() of type object<Magium\Util\Phpunit5\MasterListener> is incompatible with the declared type object<Magium\Util\Phpunit\MasterListener> of property $masterListener.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
186
            } else {
187
                self::$masterListener = new MasterListener();
188
            }
189
        }
190
        return self::$masterListener;
0 ignored issues
show
Bug Compatibility introduced by
The expression self::$masterListener; of type Magium\Util\Phpunit5\Mas...\Phpunit\MasterListener adds the type Magium\Util\Phpunit5\MasterListener to the return on line 190 which is incompatible with the return type documented by Magium\AbstractTestCase::getMasterListener of type Magium\Util\Phpunit\MasterListener.
Loading history...
191
    }
192
193
    protected function tearDown()
194
    {
195
        foreach ($this->postCallbacks as $callback) {
196
            if (is_callable($callback)) {
197
                call_user_func($callback);
198
            }
199
        }
200
        parent::tearDown();
201
        if ($this->webdriver instanceof WebDriver) {
202
            try {
203
                $this->webdriver->close();
204
            } catch (\Exception $e) {
205
                // All closed
206
            }
207
208
            $this->webdriver->quit();
209
            $this->webdriver = null;
210
        }
211
    }
212
213
214
    public function filterWebDriverAction($by)
215
    {
216
        switch ($by) {
217
            case WebDriver::BY_XPATH:
218
                return 'xpath';
219
            case WebDriver::BY_CSS_SELECTOR:
220
                return 'css_selector';
221
            case WebDriver::BY_ID:
222
                return 'id';
223
            default:
224
                return $by;
225
        }
226
    }
227
228
    public function assertElementClickable($selector, $by = WebDriver::BY_ID)
229
    {
230
        $this->elementAssertion($selector, $by, Clickable::ASSERTION);
231
    }
232
233
234
    public function assertElementNotClickable($selector, $by = WebDriver::BY_ID)
235
    {
236
        $this->elementAssertion($selector, $by, NotClickable::ASSERTION);
237
238
    }
239
240
    public static function addBaseNamespace($namespace)
241
    {
242
        if (!in_array($namespace, self::$baseNamespaces)) {
243
            self::$baseNamespaces[] = trim($namespace, '\\');
244
        }
245
    }
246
247
    public static function resolveClass( $class, $prefix = null)
248
    {
249
        $origClass = $class;
250
        if ($prefix !== null) {
251
            $class = "{$prefix}\\{$class}";
252
        }
253
        foreach (self::$baseNamespaces as $namespace) {
254
            if (strpos($class, $namespace) === 0) {
255
                // We have a fully qualified class name
256
                return $class;
257
            }
258
        }
259
260
        foreach (self::$baseNamespaces as $namespace) {
261
            $fqClass = $namespace . '\\' . $class;
262
            try {
263
                if (class_exists($fqClass)) {
264
                    return $fqClass;
265
                }
266
            } catch (\Exception $e) {
267
                /*
268
                 * Nothing to see here
269
                 * http://www.reactiongifs.us/wp-content/uploads/2015/04/nothing_to_see_here_naked_gun.gif
270
                 *
271
                 * This is necessary because, in Magento, when developer mode is turned on, Magento will throw an
272
                 * exception if the autoloader can't find a file.
273
                 */
274
            }
275
        }
276
        return $origClass;
277
    }
278
279
    public function setTypePreference($type, $preference)
280
    {
281
        $type = self::resolveClass($type);
282
        $preference = self::resolveClass($preference);
283
        $this->di->instanceManager()->unsetTypePreferences($type);
284
        $this->di->instanceManager()->setTypePreference($type, [$preference]);
285
286
    }
287
288
    protected function normalizeClassRequest($class)
289
    {
290
        return str_replace('/', '\\', $class);
291
    }
292
293
    public function addPostTestCallback($callback)
294
    {
295
        if (!is_callable($callback)) {
296
            throw new InvalidConfigurationException('Callback is not callable');
297
        }
298
        $this->postCallbacks[] = $callback;
299
    }
300
301
    /**
302
303
     * @param string $theme
304
     * @return \Magium\Themes\ThemeConfigurationInterface
305
     */
306
307
    public function getTheme($theme = null)
308
    {
309
        if ($theme === null) {
310
            return $this->get($this->baseThemeClass);
311
        }
312
        $theme = self::resolveClass($theme, 'Themes');
313
        return $this->get($theme);
314
    }
315
316
    /**
317
     *
318
     * @param string $action
319
     * @return mixed
320
     */
321
322
    public function getAction($action)
323
    {
324
        $action = self::resolveClass($action, 'Actions');
325
326
        return $this->get($action);
327
    }
328
329
330
    /**
331
     * @param string $name
332
     * @return \Magium\Identities\NameInterface
333
     */
334
335
    public function getIdentity($name = 'Customer')
336
    {
337
        $name = self::resolveClass($name, 'Identities');
338
339
        return $this->get($name);
340
    }
341
342
    /**
343
     *
344
     * @param string $navigator
345
     * @return \Magium\Navigators\NavigatorInterface
346
     */
347
348
    public function getNavigator($navigator = 'BaseMenu')
349
    {
350
        $navigator = self::resolveClass($navigator, 'Navigators');
351
352
        return $this->get($navigator);
353
    }
354
355
    public function getAssertion($assertion)
356
    {
357
        $assertion = self::resolveClass($assertion, 'Assertions');
358
359
        return $this->get($assertion);
360
    }
361
362
    /**
363
     *
364
     * @param string $extractor
365
     * @return \Magium\Extractors\AbstractExtractor
366
     */
367
368
    public function getExtractor($extractor)
369
    {
370
        $extractor = self::resolveClass($extractor, 'Extractors');
371
372
        return $this->get($extractor);
373
    }
374
375
    /**
376
     * Sleep the specified amount of time.
377
     *
378
     * Options: 1s (1 second), 1ms (1 millisecond), 1us (1 microsecond), 1ns (1 nanosecond)
379
     *
380
     * @param $time
381
     */
382
383
    public function sleep($time)
384
    {
385
        $length = (int)$time;
386
387
        if (strpos($time, 'ms') !== false) {
388
            usleep($length * 1000);
389
        } else if (strpos($time, 'us') !== false) {
390
            usleep($length);
391
        } else if (strpos($time, 'ns') !== false) {
392
            time_nanosleep(0, $length);
393
        } else {
394
            sleep($length);
395
        }
396
    }
397
398
399
    public function commandOpen($url)
400
    {
401
        $this->get(Open::class)->open($url);
402
    }
403
404
405
    /**
406
     * @return LoggerInterface
407
     */
408
409
    public function getLogger()
410
    {
411
        return $this->get(LoggerInterface::class);
412
    }
413
414
    public function get($class)
415
    {
416
        $class = $this->normalizeClassRequest($class);
417
        $preferredClass = $this->di->instanceManager()->getTypePreferences($class);
418
        if (is_array($preferredClass) && count($preferredClass) > 0) {
419
            $class = array_shift($preferredClass);
420
        }
421
        return $this->di->get($class);
422
    }
423
424
    public static function isPHPUnit5()
425
    {
426
        return class_exists('PHPUnit_Framework_TestSuite');
427
    }
428
429
    public function assertElementExists($selector, $by = 'byId')
430
    {
431
        $this->elementAssertion($selector, $by, Exists::ASSERTION);
432
    }
433
434
    public function assertTitleEquals($title)
435
    {
436
        $webTitle = $this->webdriver->getTitle();
437
        self::assertEquals($title, $webTitle);
438
    }
439
440
441
    public function assertTitleContains($title)
442
    {
443
        $webTitle = $this->webdriver->getTitle();
444
        self::assertContains($title, $webTitle);
445
    }
446
447
448
    public function assertNotTitleEquals($title)
449
    {
450
        $webTitle = $this->webdriver->getTitle();
451
        self::assertNotEquals($title, $webTitle);
452
    }
453
454
455
    public function assertNotTitleContains($title)
456
    {
457
        $webTitle = $this->webdriver->getTitle();
458
        self::assertNotContains($title, $webTitle);
459
    }
460
461
    public function assertURLEquals($url)
462
    {
463
        $webUrl = $this->webdriver->getCurrentURL();
464
        self::assertEquals($url, $webUrl);
465
    }
466
467
    public function assertURLContains($url)
468
    {
469
        $webUrl = $this->webdriver->getCurrentURL();
470
        self::assertContains($url, $webUrl);
471
    }
472
473
474
    public function assertURLNotEquals($url)
475
    {
476
        $webUrl = $this->webdriver->getCurrentURL();
477
        self::assertNotEquals($url, $webUrl);
478
    }
479
480
    public function assertURLNotContains($url)
481
    {
482
        $webUrl = $this->webdriver->getCurrentURL();
483
        self::assertNotContains($url, $webUrl);
484
    }
485
486
    protected function elementAssertion($selector, $by, $name)
487
    {
488
        $executor = $this->getAssertion(LoggingAssertionExecutor::ASSERTION);
489
        $assertion = $this->getAssertion($name);
490
        $assertion->setSelector($selector)->setBy($by);
491
        $executor->execute($assertion);
492
    }
493
494
    public function assertElementDisplayed($selector, $by = 'byId')
495
    {
496
        $this->elementAssertion($selector, $by, Displayed::ASSERTION);
497
    }
498
499
    public function assertElementNotDisplayed($selector, $by = 'byId')
500
    {
501
        $this->elementAssertion($selector, $by, NotDisplayed::ASSERTION);
502
    }
503
504
    public function assertElementNotExists($selector, $by = 'byId')
505
    {
506
        $this->elementAssertion($selector, $by, NotExists::ASSERTION);
507
    }
508
509
    /**
510
     * @return LoggingAssertionExecutor
511
     */
512
513
    public function getAssertionLogger()
514
    {
515
        return $this->getAssertion(LoggingAssertionExecutor::ASSERTION);
516
    }
517
518
    public function switchThemeConfiguration($fullyQualifiedClassName)
519
    {
520
521
        $reflection = new \ReflectionClass($fullyQualifiedClassName);
522
523
        if ($reflection->implementsInterface('Magium\Themes\ThemeConfigurationInterface')) {
524
            $this->baseThemeClass = $fullyQualifiedClassName;
525
            $this->di->instanceManager()->unsetTypePreferences('Magium\Themes\ThemeConfigurationInterface');
526
            $this->di->instanceManager()->setTypePreference('Magium\Themes\ThemeConfigurationInterface', [$fullyQualifiedClassName]);
527
528
            if ($reflection->implementsInterface('Magium\Themes\BaseThemeInterface')) {
529
                $this->di->instanceManager()->unsetTypePreferences('Magium\Themes\BaseThemeInterface');
530
                $this->di->instanceManager()->setTypePreference('Magium\Themes\BaseThemeInterface', [$fullyQualifiedClassName]);
531
            }
532
            $theme = $this->getTheme();
533
            if ($theme instanceof BaseThemeInterface) {
534
                $theme->configure($this);
535
            }
536
        } else {
537
            throw new InvalidConfigurationException('The theme configuration implement Magium\Themes\ThemeConfigurationInterface');
538
        }
539
        $this->getLogger()->addCharacteristic(LoggerInterface::CHARACTERISTIC_THEME, $fullyQualifiedClassName);
540
    }
541
542
    public static function assertWebDriverElement($element)
543
    {
544
        self::assertInstanceOf('Facebook\WebDriver\WebDriverElement', $element);
545
    }
546
547 View Code Duplication
    public function assertElementHasText($node, $text)
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...
548
    {
549
        try {
550
            $element = $this->byXpath(sprintf('//%s[contains(., "%s")]', $node, addslashes($text)));
551
            self::assertWebDriverElement($element);
552
        } catch (\Exception $e) {
553
            $this->fail('The body did not contain the text: ' . $text);
554
        }
555
    }
556
557 View Code Duplication
    public function assertPageHasText($text)
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...
558
    {
559
        try {
560
            $element = $this->webdriver->byXpath(sprintf('//body[contains(., "%s")]', $text));
561
            // If the element is not found then an exception will be thrown
562
            self::assertWebDriverElement($element);
563
        } catch (\Exception $e) {
564
            $this->fail('The body did not contain the text: ' . $text);
565
        }
566
    }
567
568
    public function assertPageNotHasText($text)
569
    {
570
        try {
571
            $this->webdriver->byXpath(sprintf('//body[contains(., "%s")]', $text));
572
            $this->fail('The page contains the words: ' . $text);
573
        } catch (NoSuchElementException $e) {
574
            // Exception thrown is a success
575
            self::assertInstanceOf(NoSuchElementException::class, $e);
576
        }
577
    }
578
579
    /**
580
     * @param $xpath
581
     * @return \Facebook\WebDriver\Remote\RemoteWebElement
582
     */
583
584
    public function byXpath($xpath)
585
    {
586
        return $this->webdriver->byXpath($xpath);
587
    }
588
589
    /**
590
     * @param $id
591
     * @return \Facebook\WebDriver\Remote\RemoteWebElement
592
     */
593
594
    public function byId($id)
595
    {
596
        return $this->webdriver->byId($id);
597
    }
598
599
    /**
600
     * @param $selector
601
     * @return \Facebook\WebDriver\Remote\RemoteWebElement
602
     */
603
604
    public function byCssSelector($selector)
605
    {
606
        return $this->webdriver->byCssSelector($selector);
607
    }
608
609
    protected function getElementByTextXpath($xpathTemplate, $text, $specificNodeType = null, $parentElementSelector = null)
610
    {
611
612
        if ($parentElementSelector !== null) {
613
            $xpathTemplate = $parentElementSelector . $xpathTemplate;
614
        }
615
        if ($specificNodeType !== null) {
616
            return $this->byXpath(sprintf($xpathTemplate, $specificNodeType, $this->getTranslator()->translatePlaceholders($text)));
617
        }
618
619
        foreach ($this->textElementNodeSearch as $nodeName) {
620
            $xpath = sprintf($xpathTemplate, $nodeName, $this->getTranslator()->translatePlaceholders($text));
621
            if ($this->webdriver->elementExists($xpath, WebDriver::BY_XPATH)) {
622
                return $this->webdriver->byXpath($xpath);
623
            }
624
        }
625
        // This is here for consistency with the other by* methods
626
        WebDriverException::throwException(7, 'Could not find element with text: ' . $this->getTranslator()->translatePlaceholders($text), []);
627
    }
628
629
    /**
630
     * @param string $text
631
     * @param string $specificNodeType
632
     * @param string $parentElementSelector
633
     * @return \Facebook\WebDriver\Remote\RemoteWebElement
634
     */
635
    public function byText($text, $specificNodeType = null, $parentElementSelector = null)
636
    {
637
        $xpathTemplate = '//%s[concat(" ",normalize-space(.)," ") = " %s "]';
638
        return $this->getElementByTextXpath($xpathTemplate, $text, $specificNodeType, $parentElementSelector);
639
    }
640
641
642
    /**
643
     * @param string $text
644
     * @param string $specificNodeType
645
     * @param string $parentElementSelector
646
     * @return \Facebook\WebDriver\Remote\RemoteWebElement
647
     */
648
    public function byContainsText($text, $specificNodeType = null, $parentElementSelector = null)
649
    {
650
        $xpathTemplate = '//%s[contains(., "%s")]';
651
        return $this->getElementByTextXpath($xpathTemplate, $text, $specificNodeType, $parentElementSelector);
652
    }
653
654
    /**
655
     * @return \Magium\Util\Translator\Translator
656
     */
657
658
    public function getTranslator()
659
    {
660
        return $this->get('Magium\Util\Translator\Translator');
661
    }
662
663
    public function addTranslationCsvFile($file, $locale)
664
    {
665
        $this->getTranslator()->addTranslationCsvFile($file, $locale);
666
    }
667
}
668