Passed
Push — master ( 9a39c2...62b971 )
by Vitaly
09:11
created

GenericFeatureContext   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 308
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 9

Importance

Changes 0
Metric Value
wmc 27
lcom 2
cbo 9
dl 0
loc 308
rs 10
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getSymfonyService() 0 8 1
B spin() 0 23 5
A takeScreenShotAfterFailedStep() 0 14 3
A findAllByCssSelector() 0 20 2
A findByCssSelector() 0 4 1
A iSetBrowserWindowSizeToX() 0 4 1
A iWaitMillisecondsForResponse() 0 4 1
A iClickOnTheElement() 0 5 1
A assertPageAddress() 0 6 1
A iHoverOverTheElement() 0 4 1
A iFillInTheElement() 0 4 1
A iScrollVerticallyToPx() 0 4 1
A iScrollHorizontallyToPx() 0 4 1
A iFillHiddenFieldWith() 0 7 1
A iCheckCustomCheckboxWith() 0 13 3
A dragElementTo() 0 6 1
A iFillInTheElementUsingJs() 0 4 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace samsonframework\behatextension;
6
7
use Behat\Behat\Hook\Scope\AfterStepScope;
8
use Behat\Mink\Driver\Selenium2Driver;
9
use Behat\Mink\Element\NodeElement;
10
use Behat\MinkExtension\Context\MinkContext;
11
12
/**
13
 * Defines generic feature steps.
14
 */
15
class GenericFeatureContext extends MinkContext
16
{
17
    /** @var int UI generic delay duration in milliseconds */
18
    const DELAY = 1000;
19
    /** @var 0.1 sec spin delay */
20
    const SPIN_DELAY = 100000;
21
    /** @var int UI javascript generic delay duration in milliseconds */
22
    const JS_DELAY = self::DELAY / 5;
23
    /** @var int UI spin function timeout for ex 30*0.1s = 15 sec timeout */
24
    const SPIN_TIMEOUT = 150;
25
26
    /** @var mixed */
27
    protected $session;
28
29
    /**
30
     * Initializes context.
31
     *
32
     * Every scenario gets its own context instance.
33
     * You can also pass arbitrary arguments to the
34
     * context constructor through behat.yml.
35
     *
36
     * @param mixed $session
37
     */
38
    public function __construct($session = null)
39
    {
40
        $this->session = $session;
41
42
        ini_set('xdebug.max_nesting_level', '1000');
43
    }
44
45
    /**
46
     * Get Symfony service instance.
47
     *
48
     * @param string $serviceName Service identifier
49
     * @param string $session     Behat Symfony session name
50
     *
51
     * @return object Symfony service instance
52
     */
53
    public function getSymfonyService(string $serviceName, string $session = 'symfony2')
54
    {
55
        return $this->getSession($session)
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Mink\Driver\DriverInterface as the method getClient() does only exist in the following implementations of said interface: Behat\Mink\Driver\BrowserKitDriver, Behat\Symfony2Extension\Driver\KernelDriver.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
56
            ->getDriver()
57
            ->getClient()
58
            ->getContainer()
59
            ->get($serviceName);
60
    }
61
62
    /**
63
     * Spin function to avoid Selenium fails.
64
     *
65
     * @param callable $lambda
66
     * @param null     $data
67
     * @param int      $delay
68
     * @param int      $timeout
69
     *
70
     * @throws \Exception
71
     *
72
     * @return bool
73
     */
74
    public function spin(callable $lambda, $data = null, $delay = self::SPIN_DELAY, $timeout = self::SPIN_TIMEOUT)
75
    {
76
        $failedExceptions = [];
77
        for ($i = 0; $i < $timeout; $i++) {
78
            try {
79
                if ($lambda($this, $data)) {
80
                    return true;
81
                }
82
            } catch (\Exception $e) { // Gather unique exceptions
83
                $failedExceptions[$e->getMessage()] = $e->getMessage();
84
            }
85
86
            usleep($delay);
87
        }
88
89
        $backtrace = debug_backtrace();
90
91
        throw new \Exception(
92
            'Timeout thrown by '.$backtrace[1]['class'].'::'.$backtrace[1]['function']."()\n"
93
            .(array_key_exists('file', $backtrace[1]) ? $backtrace[1]['file'].', line '.$backtrace[1]['line'] : '')."\n"
94
            .implode("\n", $failedExceptions)
95
        );
96
    }
97
98
    /**
99
     * @AfterStep
100
     *
101
     * @param AfterStepScope $scope
102
     */
103
    public function takeScreenShotAfterFailedStep(AfterStepScope $scope)
104
    {
105
        if (99 === $scope->getTestResult()->getResultCode()) {
106
            $driver = $this->getSession()->getDriver();
107
108
            if (!($driver instanceof Selenium2Driver)) {
109
                return;
110
            }
111
112
            $step = $scope->getStep();
113
            $fileName = 'Fail.'.preg_replace('/[^a-zA-Z0-9-_\.]/', '_', $scope->getName().'-'.$step->getText()).'.jpg';
114
            file_put_contents($fileName, $driver->getScreenshot());
115
        }
116
    }
117
118
    /**
119
     * Find all elements by CSS selector.
120
     *
121
     * @param string $selector CSS selector
122
     *
123
     * @throws \InvalidArgumentException If element not found
124
     *
125
     * @return \Behat\Mink\Element\NodeElement[]
126
     */
127
    protected function findAllByCssSelector(string $selector)
128
    {
129
        $elements = [];
130
131
        $this->spin(function (MinkContext $context) use ($selector, &$elements) {
132
            $session = $context->getSession();
133
134
            $elements = $session->getPage()->findAll('css', $context->fixStepArgument($selector));
135
136
            // If element with current selector is not found then print error
137
            if (count($elements) === 0) {
138
                throw new \InvalidArgumentException(sprintf('Could not evaluate CSS selector: "%s"', $selector));
139
            }
140
141
            return true;
142
        });
143
144
145
        return $elements;
146
    }
147
148
    /**
149
     * Find element by CSS selector.
150
     *
151
     * @param string $selector CSS selector
152
     *
153
     * @throws \InvalidArgumentException If element not found
154
     *
155
     * @return \Behat\Mink\Element\NodeElement
156
     */
157
    public function findByCssSelector(string $selector) : NodeElement
158
    {
159
        return $this->findAllByCssSelector($selector)[0];
160
    }
161
162
    /**
163
     * @Given /^I set browser window size to "([^"]*)" x "([^"]*)"$/
164
     *
165
     * @param int $width  Browser window width
166
     * @param int $height Browser window height
167
     */
168
    public function iSetBrowserWindowSizeToX($width, $height)
169
    {
170
        $this->getSession()->resizeWindow((int) $width, (int) $height, 'current');
171
    }
172
173
    /**
174
     * @Given /^I wait "([^"]*)" milliseconds for response$/
175
     *
176
     * @param int $delay Amount of milliseconds to wait
177
     */
178
    public function iWaitMillisecondsForResponse($delay = self::DELAY)
179
    {
180
        $this->getSession()->wait((int) $delay);
181
    }
182
183
    /**
184
     * Click on the element with the provided xpath query.
185
     *
186
     * @When I click on the element :arg1
187
     *
188
     * @param string $selector CSS element selector
189
     *
190
     * @throws \InvalidArgumentException
191
     */
192
    public function iClickOnTheElement(string $selector)
193
    {
194
        // Click on the founded element
195
        $this->findByCssSelector($selector)->click();
196
    }
197
198
    /**
199
     * Checks, that current page PATH is equal to specified
200
     * Example: Then I should be on "/"
201
     * Example: And I should be on "/bats"
202
     * Example: And I should be on "http://google.com"
203
     *
204
     * @param string $page Page for assertion
205
     */
206
    public function assertPageAddress($page) {
207
        $this->spin(function (MinkContext $context) use ($page) {
208
            $context->assertSession()->addressEquals($this->locatePath($page));
209
            return true;
210
        });
211
    }
212
213
    /**
214
     * @When /^I hover over the element "([^"]*)"$/
215
     *
216
     * @param string $selector CSS element selector
217
     *
218
     * @throws \InvalidArgumentException
219
     */
220
    public function iHoverOverTheElement(string $selector)
221
    {
222
        $this->findByCssSelector($selector)->mouseOver();
223
    }
224
225
    /**
226
     * Fill in input with the provided info.
227
     *
228
     * @When I fill in the input :arg1 with :arg2
229
     *
230
     * @param string $selector CSS element selector
231
     * @param string $value    Element value for filling in
232
     *
233
     * @throws \InvalidArgumentException
234
     */
235
    public function iFillInTheElement(string $selector, string $value)
236
    {
237
        $this->findByCssSelector($selector)->setValue($this->fixStepArgument($value));
238
    }
239
240
    /**
241
     * @When I scroll vertically to :arg1 px
242
     *
243
     * @param mixed $yPos Vertical scrolling position in pixels
244
     */
245
    public function iScrollVerticallyToPx($yPos)
246
    {
247
        $this->getSession()->executeScript('window.scrollTo(0, Math.min(document.documentElement.scrollHeight, document.body.scrollHeight, '.((int) $yPos).'));');
248
    }
249
250
    /**
251
     * @When I scroll horizontally to :arg1 px
252
     *
253
     * @param mixed $xPos Horizontal scrolling position in pixels
254
     */
255
    public function iScrollHorizontallyToPx($xPos)
256
    {
257
        $this->getSession()->executeScript('window.scrollTo('.((int) $xPos).', 0);');
258
    }
259
260
    /**
261
     * @Given /^I fill hidden field "([^"]*)" with "([^"]*)"$/
262
     *
263
     * @param string $field Field name
264
     * @param string $value Field value
265
     */
266
    public function iFillHiddenFieldWith(string $field, string $value)
267
    {
268
        // TODO: Change to Mink implementation
269
        $this->getSession()->executeScript("
270
            $('input[name=".$field."]').val('".$value."');
271
        ");
272
    }
273
274
    /**
275
     * @Then I check custom checkbox with :id
276
     *
277
     * @param string $id Checkbox identifier
278
     *
279
     * @throws \InvalidArgumentException If checkbox with provided identifier does not exists
280
     */
281
    public function iCheckCustomCheckboxWith(string $id)
282
    {
283
        // Find label for checkbox by chekbox identifier
284
        $element = null;
285
        foreach ($this->findAllByCssSelector('label') as $label) {
286
            if ($label->getAttribute('for') === $id) {
287
                $element = $label;
288
            }
289
        }
290
291
        // Imitate checkbox checking by clicking its label
292
        $element->click();
293
    }
294
295
    /**
296
     * @Then I drag element :selector to :target
297
     *
298
     * @param string $selector Source element for dragging
299
     * @param string $target   Target element to drag to
300
     *
301
     * @throws \InvalidArgumentException
302
     */
303
    public function dragElementTo(string $selector, string $target)
304
    {
305
        $this->findByCssSelector($selector)->dragTo($this->findByCssSelector($target));
306
307
        // $this->iWaitMillisecondsForResponse(self::JS_DELAY);
308
    }
309
310
    /**
311
     * Fill in input with the provided info.
312
     *
313
     * @When I fill in the element :arg1 with value :arg2 using js
314
     *
315
     * @param string $selector CSS element selector
316
     * @param string $value    Element value for filling in
317
     */
318
    public function iFillInTheElementUsingJs(string $selector, string $value)
319
    {
320
        $this->getSession()->executeScript('document.querySelectorAll("'.$selector.'")[0].value="'.$value.'";');
321
    }
322
}
323