Completed
Push — master ( 62b971...fca48f )
by Vitaly
01:54
created

GenericFeatureContext::iShouldBeOnPage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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