TqContext::workWithElementsInRegion()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 0
cts 7
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
crap 6
1
<?php
2
/**
3
 * @author Sergii Bondarenko, <[email protected]>
4
 */
5
namespace Drupal\TqExtension\Context;
6
7
// Scope definitions.
8
use Behat\Behat\Hook\Scope;
9
// Utils.
10
use Drupal\TqExtension\Utils\Database\Database;
11
use Drupal\TqExtension\Utils\LogicalAssertion;
12
13
class TqContext extends RawTqContext
14
{
15
    use LogicalAssertion;
16
17
    /**
18
     * The name and working element of main window.
19
     *
20
     * @var array
21
     */
22
    private $mainWindow = [];
23
    /**
24
     * @var Database
25
     */
26
    private static $database;
27
28
    /**
29
     * Supports switching between the two windows only.
30
     *
31
     * @Given /^(?:|I )switch to opened window$/
32
     * @Then /^(?:|I )switch back to main window$/
33
     */
34
    public function iSwitchToWindow()
35
    {
36
        $windows = $this->getWindowNames();
37
38
        // If the window was not switched yet, then store it name and working element for future switching back.
39
        if (empty($this->mainWindow)) {
40
            $this->mainWindow['name'] = array_shift($windows);
41
            $this->mainWindow['element'] = $this->getWorkingElement();
42
43
            $window = reset($windows);
44
        } else {
45
            $window = $this->mainWindow['name'];
46
            $element = $this->mainWindow['element'];
47
48
            $this->mainWindow = [];
49
        }
50
51
        $this->getSession()->switchToWindow($window);
52
        $this->setWorkingElement(isset($element) ? $element : $this->getBodyElement());
53
    }
54
55
    /**
56
     * @Given /^(?:|I )switch to CKFinder window$/
57
     *
58
     * @javascript
59
     */
60
    public function switchToCKFinderWindow()
61
    {
62
        $this->iSwitchToWindow();
63
        $this->executeJsOnElement(
64
            $this->element('css', 'iframe'),
65
            "{{ELEMENT}}.setAttribute('id', 'behat_ckfinder');"
66
        );
67
        $this->iSwitchToAnIframe('behat_ckfinder');
68
    }
69
70
    /**
71
     * @param string $name
72
     *   An iframe name (null for switching back).
73
     *
74
     * @Given /^(?:|I )switch to an iframe "([^"]*)"$/
75
     * @Then /^(?:|I )switch back from an iframe$/
76
     */
77
    public function iSwitchToAnIframe($name = null)
78
    {
79
        $this->getSession()->switchToIFrame($name);
80
    }
81
82
    /**
83
     * Open the page with specified resolution.
84
     *
85
     * @param string $width_height
86
     *   String that satisfy the condition "<WIDTH>x<HEIGHT>".
87
     *
88
     * @example
89
     * Given I should use the "1280x800" resolution
90
     *
91
     * @Given /^(?:|I should )use the "([^"]*)" screen resolution$/
92
     */
93
    public function useScreenResolution($width_height)
94
    {
95
        list($width, $height) = explode('x', $width_height);
96
97
        $this->getSessionDriver()->resizeWindow((int) $width, (int) $height);
98
    }
99
100
    /**
101
     * @param string $action
102
     *   The next actions can be: "press", "click", "double click" and "right click".
103
     * @param string $selector
104
     *   CSS, inaccurate text or selector name from behat.yml can be used.
105
     *
106
     * @throws \WebDriver\Exception\NoSuchElement
107
     *   When element was not found.
108
     *
109
     * @Given /^(?:|I )((?:|(?:double|right) )click|press) on "([^"]*)"$/
110
     */
111
    public function pressElement($action, $selector)
112
    {
113
        // 1. Get the action, divide string by spaces and put it parts into an array.
114
        // 2. Apply the "ucfirst" function for each array element.
115
        // 3. Make string from an array.
116
        // 4. Set the first letter of a string to lower case.
117
        $this->element('*', $selector)->{lcfirst(implode(array_map('ucfirst', explode(' ', $action))))}();
118
    }
119
120
    /**
121
     * @Given /^(?:|I )wait until AJAX is finished$/
122
     *
123
     * @javascript
124
     */
125
    public function waitUntilAjaxIsFinished()
126
    {
127
        $this->waitAjaxAndAnimations();
128
    }
129
130
    /**
131
     * @param string $selector
132
     *   CSS selector or region name.
133
     *
134
     * @Then /^(?:|I )work with elements in "([^"]*)"(?:| region)$/
135
     */
136
    public function workWithElementsInRegion($selector)
137
    {
138
        if (in_array($selector, ['html', 'head'])) {
139
            $element = $this->getSession()->getPage()->find('css', $selector);
140
        } else {
141
            $element = $this->element('css', $selector);
142
        }
143
144
        $this->setWorkingElement($element);
0 ignored issues
show
Bug introduced by
It seems like $element defined by $this->getSession()->get...>find('css', $selector) on line 139 can be null; however, Drupal\TqExtension\Conte...xt::setWorkingElement() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
145
    }
146
147
    /**
148
     * @Then /^(?:|I )checkout to whole page$/
149
     */
150
    public function unsetWorkingElementScope()
151
    {
152
        $this->unsetWorkingElement();
153
    }
154
155
    /**
156
     * @param int $seconds
157
     *   Amount of seconds when nothing to happens.
158
     *
159
     * @Given /^(?:|I )wait (\d+) seconds$/
160
     */
161
    public function waitSeconds($seconds)
162
    {
163
        sleep($seconds);
164
    }
165
166
    /**
167
     * @param string $selector
168
     *   Text or CSS.
169
     *
170
     * @throws \Exception
171
     *
172
     * @Given /^(?:|I )scroll to "([^"]*)" element$/
173
     *
174
     * @javascript
175
     */
176
    public function scrollToElement($selector)
177
    {
178
        if (!self::hasTag('javascript')) {
179
            throw new \Exception('Scrolling to an element is impossible without a JavaScript.');
180
        }
181
182
        $this->executeJsOnElement($this->findElement($selector), '{{ELEMENT}}.scrollIntoView(true);');
0 ignored issues
show
Bug introduced by
It seems like $this->findElement($selector) can be null; however, executeJsOnElement() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
183
    }
184
185
    /**
186
     * @param string $message
187
     *   JS error.
188
     * @param bool $negate
189
     *   Whether page should or should not contain the error.
190
     * @param string $file
191
     *   File where error appears.
192
     *
193
     * @throws \RuntimeException
194
     * @throws \Exception
195
     *
196
     * @example
197
     * Then check that "TypeError: cell[0] is undefined" JS error appears in "misc/tabledrag.js" file
198
     *
199
     * @Then /^check that "([^"]*)" JS error(| not) appears in "([^"]*)" file$/
200
     *
201
     * @javascript
202
     */
203
    public function checkJavaScriptError($message, $negate, $file)
204
    {
205
        $errors = $this->getSession()->evaluateScript('return JSON.stringify(window.errors);');
206
        $negate = (bool) $negate;
207
208
        if (empty($errors)) {
209
            if (!$negate) {
210
                throw new \RuntimeException('Page does not contain JavaScript errors.');
211
            }
212
        } else {
213
            $base_url = $this->locatePath();
214
215
            foreach (json_decode($errors) as $error) {
216
                $error->location = str_replace($base_url, '', $error->location);
217
218
                switch (static::assertion(
219
                    strpos($error->message, $message) === 0 && strpos($error->location, $file) === 0,
220
                    $negate
221
                )) {
222
                    case 1:
223
                        throw new \Exception(sprintf(
224
                            'The "%s" error found in "%s" file, but should not be.',
225
                            $message,
226
                            $file
227
                        ));
228
229
                    case 2:
230
                        throw new \Exception(sprintf(
231
                            'The "%s" error not found in "%s" file, but should be.',
232
                            $message,
233
                            $file
234
                        ));
235
                }
236
            }
237
        }
238
    }
239
240
    /**
241
     * @param string $selector
242
     * @param string $attribute
243
     * @param string $expectedValue
244
     *
245
     * @throws \Exception
246
     *
247
     * @example
248
     * Then I should see the "#table_cell" element with "colspan" attribute having "3" value
249
     *
250
     * @Then /^(?:|I )should see the "([^"]*)" element with "([^"]*)" attribute having "([^"]*)" value$/
251
     */
252
    public function assertElementAttribute($selector, $attribute, $expectedValue)
253
    {
254
        $actualValue = $this->element('*', $selector)->getAttribute($attribute);
255
256
        if (null === $actualValue) {
257
            throw new \InvalidArgumentException(sprintf(
258
                'Element does not contain the "%s" attribute.',
259
                $attribute
260
            ));
261
        } elseif ($actualValue !== $expectedValue) {
262
            throw new \Exception(sprintf(
263
                'Attribute "%s" have the "%s" value which is not equal to "%s".',
264
                $attribute,
265
                $actualValue,
266
                $expectedValue
267
            ));
268
        }
269
    }
270
271
    /**
272
     * @param Scope\BeforeFeatureScope $scope
273
     *   Scope of the processing feature.
274
     *
275
     * @BeforeFeature
276
     */
277
    public static function beforeFeature(Scope\BeforeFeatureScope $scope)
278
    {
279
        self::collectTags($scope->getFeature()->getTags());
280
281
        // Database will be cloned for every feature with @cloneDB tag.
282
        if (self::hasTag('clonedb')) {
283
            self::$database = clone new Database(self::getTag('clonedb', 'default'));
284
        }
285
286
        static::setDrupalVariables([
287
            // Set to "false", because the administration menu will not be rendered.
288
            // @see https://www.drupal.org/node/2023625#comment-8607207
289
            'admin_menu_cache_client' => false,
290
        ]);
291
292
        static::injectCustomJavascript('CatchErrors');
293
    }
294
295
    /**
296
     * @AfterFeature
297
     */
298
    public static function afterFeature()
299
    {
300
        // Restore initial database when feature is done (call __destruct).
301
        self::$database = null;
302
303
        // Remove injected script.
304
        static::injectCustomJavascript('CatchErrors', true);
305
    }
306
307
    /**
308
     * @param Scope\BeforeScenarioScope $scope
309
     *   Scope of the processing scenario.
310
     *
311
     * @BeforeScenario
312
     */
313
    public function beforeScenario(Scope\BeforeScenarioScope $scope)
314
    {
315
        self::collectTags($scope->getScenario()->getTags());
316
317
        // No need to keep working element between scenarios.
318
        $this->unsetWorkingElement();
319
        // Any page should be visited due to using jQuery and checking the cookies.
320
        $this->visitPath('/');
321
        // By "Goutte" session we need to visit any page to be able to set a cookie
322
        // for this session and use it for checking request status codes.
323
        $this->visitPath('/', 'goutte');
324
    }
325
326
    /**
327
     * Set the jQuery handlers for "start" and "finish" events of AJAX queries.
328
     * In each method can be used the "waitAjaxAndAnimations" method for check
329
     * that AJAX was finished.
330
     *
331
     * @see RawTqContext::waitAjaxAndAnimations()
332
     *
333
     * @BeforeScenario @javascript
334
     */
335
    public function beforeScenarioJS()
336
    {
337
        $javascript = '';
338
339
        foreach (['Start' => 'true', 'Complete' => 'false'] as $event => $state) {
340
            $javascript .= "$(document).bind('ajax$event', function() {window.__behatAjax = $state;});";
341
        }
342
343
        $this->executeJs($javascript);
344
    }
345
346
    /**
347
     * IMPORTANT! The "BeforeStep" hook should not be tagged, because steps has no tags!
348
     *
349
     * @param Scope\StepScope|Scope\BeforeStepScope $scope
350
     *   Scope of the processing step.
351
     *
352
     * @BeforeStep
353
     */
354
    public function beforeStep(Scope\StepScope $scope)
0 ignored issues
show
Unused Code introduced by
The parameter $scope 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...
Coding Style introduced by
beforeStep uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
355
    {
356
        self::$pageUrl = $this->getCurrentUrl();
357
        // To allow Drupal use its internal, web-based functionality, such as "arg()" or "current_path()" etc.
358
        $_GET['q'] = ltrim(parse_url(static::$pageUrl)['path'], '/');
359
        drupal_path_initialize();
360
    }
361
362
    /**
363
     * IMPORTANT! The "AfterStep" hook should not be tagged, because steps has no tags!
364
     *
365
     * @param Scope\StepScope|Scope\AfterStepScope $scope
366
     *   Scope of the processing step.
367
     *
368
     * @AfterStep
369
     */
370
    public function afterStep(Scope\StepScope $scope)
371
    {
372
        // If "mainWindow" variable is not empty that means that additional window has been opened.
373
        // Then, if number of opened windows equals to one, we need to switch back to original window,
374
        // otherwise an error will occur: "Window not found. The browser window may have been closed".
375
        // This happens due to auto closing window by JavaScript (CKFinder does this after choosing a file).
376
        if (!empty($this->mainWindow) && count($this->getWindowNames()) == 1) {
377
            $this->iSwitchToWindow();
378
        }
379
380
        if (self::hasTag('javascript') && self::isStepImpliesJsEvent($scope)) {
381
            $this->waitAjaxAndAnimations();
382
        }
383
    }
384
}
385