Completed
Pull Request — master (#16)
by Sergii
03:45
created

TqContext   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 13

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 35
lcom 2
cbo 13
dl 0
loc 359
ccs 0
cts 123
cp 0
rs 9
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A iSwitchToWindow() 0 20 3
A switchToCKFinderWindow() 0 9 1
A iSwitchToAnIframe() 0 4 1
A useScreenResolution() 0 6 1
A pressElement() 0 8 1
A waitUntilAjaxIsFinished() 0 4 1
A workWithElementsInRegion() 0 10 2
A unsetWorkingElementScope() 0 4 1
A waitSeconds() 0 4 1
A scrollToElement() 0 8 2
C checkJavaScriptError() 0 36 7
A assertElementAttribute() 0 14 3
A beforeFeature() 0 12 2
A afterFeature() 0 8 1
A beforeScenario() 0 9 1
A beforeScenarioJS() 0 10 2
A beforeStep() 0 6 1
A afterStep() 0 14 4
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 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
        foreach ($this->findAll($selector) as $element) {
255
            if ($element->getAttribute($attribute) === $expectedValue) {
256
                return;
257
            }
258
        }
259
260
        throw new \InvalidArgumentException(sprintf(
261
            'No elements with "%s" attribute have been found by "%s" selector.',
262
            $attribute,
263
            $selector
264
        ));
265
    }
266
267
    /**
268
     * @param Scope\BeforeFeatureScope $scope
269
     *   Scope of the processing feature.
270
     *
271
     * @BeforeFeature
272
     */
273
    public static function beforeFeature(Scope\BeforeFeatureScope $scope)
274
    {
275
        self::collectTags($scope->getFeature()->getTags());
276
277
        // Database will be cloned for every feature with @cloneDB tag.
278
        if (self::hasTag('clonedb')) {
279
            self::$database = clone new Database(self::getTag('clonedb', 'default'));
280
        }
281
282
        \DrupalKernelPlaceholder::beforeFeature($scope);
283
        \DrupalKernelPlaceholder::injectCustomJavascript('CatchErrors');
284
    }
285
286
    /**
287
     * @AfterFeature
288
     */
289
    public static function afterFeature()
290
    {
291
        // Restore initial database when feature is done (call __destruct).
292
        self::$database = null;
293
294
        // Remove injected script.
295
        \DrupalKernelPlaceholder::injectCustomJavascript('CatchErrors', true);
296
    }
297
298
    /**
299
     * @param Scope\BeforeScenarioScope $scope
300
     *   Scope of the processing scenario.
301
     *
302
     * @BeforeScenario
303
     */
304
    public function beforeScenario(Scope\BeforeScenarioScope $scope)
305
    {
306
        self::collectTags($scope->getScenario()->getTags());
307
308
        // No need to keep working element between scenarios.
309
        $this->unsetWorkingElement();
310
        // Any page should be visited due to using jQuery and checking the cookies.
311
        $this->getRedirectContext()->visitPage('/');
312
    }
313
314
    /**
315
     * Set the jQuery handlers for "start" and "finish" events of AJAX queries.
316
     * In each method can be used the "waitAjaxAndAnimations" method for check
317
     * that AJAX was finished.
318
     *
319
     * @see RawTqContext::waitAjaxAndAnimations()
320
     *
321
     * @BeforeScenario @javascript
322
     */
323
    public function beforeScenarioJS()
324
    {
325
        $javascript = '';
326
327
        foreach (['Start' => 'true', 'Complete' => 'false'] as $event => $state) {
328
            $javascript .= "$(document).bind('ajax$event', function() {window.__behatAjax = $state;});";
329
        }
330
331
        $this->executeJs($javascript);
332
    }
333
334
    /**
335
     * IMPORTANT! The "BeforeStep" hook should not be tagged, because steps has no tags!
336
     *
337
     * @param Scope\StepScope|Scope\BeforeStepScope $scope
338
     *   Scope of the processing step.
339
     *
340
     * @BeforeStep
341
     */
342
    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...
343
    {
344
        self::$pageUrl = $this->getCurrentUrl();
345
        // To allow Drupal use its internal, web-based functionality, such as "arg()" or "current_path()" etc.
346
        \DrupalKernelPlaceholder::setCurrentPath(ltrim(parse_url(static::$pageUrl)['path'], '/'));
347
    }
348
349
    /**
350
     * IMPORTANT! The "AfterStep" hook should not be tagged, because steps has no tags!
351
     *
352
     * @param Scope\StepScope|Scope\AfterStepScope $scope
353
     *   Scope of the processing step.
354
     *
355
     * @AfterStep
356
     */
357
    public function afterStep(Scope\StepScope $scope)
358
    {
359
        // If "mainWindow" variable is not empty that means that additional window has been opened.
360
        // Then, if number of opened windows equals to one, we need to switch back to original window,
361
        // otherwise an error will occur: "Window not found. The browser window may have been closed".
362
        // This happens due to auto closing window by JavaScript (CKFinder does this after choosing a file).
363
        if (!empty($this->mainWindow) && count($this->getWindowNames()) == 1) {
364
            $this->iSwitchToWindow();
365
        }
366
367
        if (self::isStepImpliesJsEvent($scope)) {
368
            $this->waitAjaxAndAnimations();
369
        }
370
    }
371
}
372