Completed
Pull Request — master (#16)
by Sergii
04:51
created

TqContext::beforeScenario()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 0
cts 2
cp 0
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
crap 2
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
use Drupal\TqExtension\Cores\DrupalKernelPlaceholder;
13
14
class TqContext extends RawTqContext
15
{
16
    use LogicalAssertion;
17
18
    /**
19
     * The name and working element of main window.
20
     *
21
     * @var array
22
     */
23
    private $mainWindow = [];
24
    /**
25
     * @var Database
26
     */
27
    private static $database;
28
29
    /**
30
     * Supports switching between the two windows only.
31
     *
32
     * @Given /^(?:|I )switch to opened window$/
33
     * @Then /^(?:|I )switch back to main window$/
34
     */
35
    public function iSwitchToWindow()
36
    {
37
        $windows = $this->getWindowNames();
38
39
        // If the window was not switched yet, then store it name and working element for future switching back.
40
        if (empty($this->mainWindow)) {
41
            $this->mainWindow['name'] = array_shift($windows);
42
            $this->mainWindow['element'] = $this->getWorkingElement();
43
44
            $window = reset($windows);
45
        } else {
46
            $window = $this->mainWindow['name'];
47
            $element = $this->mainWindow['element'];
48
49
            $this->mainWindow = [];
50
        }
51
52
        $this->getSession()->switchToWindow($window);
53
        $this->setWorkingElement(isset($element) ? $element : $this->getBodyElement());
54
    }
55
56
    /**
57
     * @Given /^(?:|I )switch to CKFinder window$/
58
     *
59
     * @javascript
60
     */
61
    public function switchToCKFinderWindow()
62
    {
63
        $this->iSwitchToWindow();
64
        $this->executeJsOnElement(
65
            $this->element('css', 'iframe'),
66
            "{{ELEMENT}}.setAttribute('id', 'behat_ckfinder');"
67
        );
68
        $this->iSwitchToAnIframe('behat_ckfinder');
69
    }
70
71
    /**
72
     * @param string $name
73
     *   An iframe name (null for switching back).
74
     *
75
     * @Given /^(?:|I )switch to an iframe "([^"]*)"$/
76
     * @Then /^(?:|I )switch back from an iframe$/
77
     */
78
    public function iSwitchToAnIframe($name = null)
79
    {
80
        $this->getSession()->switchToIFrame($name);
81
    }
82
83
    /**
84
     * Open the page with specified resolution.
85
     *
86
     * @param string $width_height
87
     *   String that satisfy the condition "<WIDTH>x<HEIGHT>".
88
     *
89
     * @example
90
     * Given I should use the "1280x800" resolution
91
     *
92
     * @Given /^(?:|I should )use the "([^"]*)" screen resolution$/
93
     */
94
    public function useScreenResolution($width_height)
95
    {
96
        list($width, $height) = explode('x', $width_height);
97
98
        $this->getSessionDriver()->resizeWindow((int) $width, (int) $height);
99
    }
100
101
    /**
102
     * @param string $action
103
     *   The next actions can be: "press", "click", "double click" and "right click".
104
     * @param string $selector
105
     *   CSS, inaccurate text or selector name from behat.yml can be used.
106
     *
107
     * @throws \WebDriver\Exception\NoSuchElement
108
     *   When element was not found.
109
     *
110
     * @Given /^(?:|I )((?:|(?:double|right) )click|press) on "([^"]*)"$/
111
     */
112
    public function pressElement($action, $selector)
113
    {
114
        // 1. Get the action, divide string by spaces and put it parts into an array.
115
        // 2. Apply the "ucfirst" function for each array element.
116
        // 3. Make string from an array.
117
        // 4. Set the first letter of a string to lower case.
118
        $this->element('*', $selector)->{lcfirst(implode(array_map('ucfirst', explode(' ', $action))))}();
119
    }
120
121
    /**
122
     * @Given /^(?:|I )wait until AJAX is finished$/
123
     *
124
     * @javascript
125
     */
126
    public function waitUntilAjaxIsFinished()
127
    {
128
        $this->waitAjaxAndAnimations();
129
    }
130
131
    /**
132
     * @param string $selector
133
     *   CSS selector or region name.
134
     *
135
     * @Then /^(?:|I )work with elements in "([^"]*)"(?:| region)$/
136
     */
137
    public function workWithElementsInRegion($selector)
138
    {
139
        if (in_array($selector, ['html', 'head'])) {
140
            $element = $this->getSession()->getPage()->find('css', $selector);
141
        } else {
142
            $element = $this->element('css', $selector);
143
        }
144
145
        $this->setWorkingElement($element);
0 ignored issues
show
Bug introduced by
It seems like $element defined by $this->getSession()->get...>find('css', $selector) on line 140 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...
146
    }
147
148
    /**
149
     * @Then /^(?:|I )checkout to whole page$/
150
     */
151
    public function unsetWorkingElementScope()
152
    {
153
        $this->unsetWorkingElement();
154
    }
155
156
    /**
157
     * @param int $seconds
158
     *   Amount of seconds when nothing to happens.
159
     *
160
     * @Given /^(?:|I )wait (\d+) seconds$/
161
     */
162
    public function waitSeconds($seconds)
163
    {
164
        sleep($seconds);
165
    }
166
167
    /**
168
     * @param string $selector
169
     *   Text or CSS.
170
     *
171
     * @throws \Exception
172
     *
173
     * @Given /^(?:|I )scroll to "([^"]*)" element$/
174
     *
175
     * @javascript
176
     */
177
    public function scrollToElement($selector)
178
    {
179
        if (!self::hasTag('javascript')) {
180
            throw new \Exception('Scrolling to an element is impossible without JavaScript.');
181
        }
182
183
        $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...
184
    }
185
186
    /**
187
     * @param string $message
188
     *   JS error.
189
     * @param bool $negate
190
     *   Whether page should or should not contain the error.
191
     * @param string $file
192
     *   File where error appears.
193
     *
194
     * @throws \RuntimeException
195
     * @throws \Exception
196
     *
197
     * @example
198
     * Then check that "TypeError: cell[0] is undefined" JS error appears in "misc/tabledrag.js" file
199
     *
200
     * @Then /^check that "([^"]*)" JS error(| not) appears in "([^"]*)" file$/
201
     *
202
     * @javascript
203
     */
204
    public function checkJavaScriptError($message, $negate, $file)
205
    {
206
        $errors = $this->getSession()->evaluateScript('return JSON.stringify(window.errors);');
207
        $negate = (bool) $negate;
208
209
        if (empty($errors)) {
210
            if (!$negate) {
211
                throw new \RuntimeException('Page does not contain JavaScript errors.');
212
            }
213
        } else {
214
            $base_url = $this->locatePath();
215
216
            foreach (json_decode($errors) as $error) {
217
                $error->location = str_replace($base_url, '', $error->location);
218
219
                switch (static::assertion(
220
                    strpos($error->message, $message) === 0 && strpos($error->location, $file) === 0,
221
                    $negate
222
                )) {
223
                    case 1:
224
                        throw new \Exception(sprintf(
225
                            'The "%s" error found in "%s" file, but should not be.',
226
                            $message,
227
                            $file
228
                        ));
229
230
                    case 2:
231
                        throw new \Exception(sprintf(
232
                            'The "%s" error not found in "%s" file, but should be.',
233
                            $message,
234
                            $file
235
                        ));
236
                }
237
            }
238
        }
239
    }
240
241
    /**
242
     * @param string $selector
243
     * @param string $attribute
244
     * @param string $expectedValue
245
     *
246
     * @throws \Exception
247
     *
248
     * @example
249
     * Then I should see the "#table_cell" element with "colspan" attribute having "3" value
250
     *
251
     * @Then /^(?:|I )should see the "([^"]*)" element with "([^"]*)" attribute having "([^"]*)" value$/
252
     */
253
    public function assertElementAttribute($selector, $attribute, $expectedValue)
254
    {
255
        foreach ($this->findAll($selector) as $element) {
256
            if ($element->getAttribute($attribute) === $expectedValue) {
257
                return;
258
            }
259
        }
260
261
        throw new \InvalidArgumentException(sprintf(
262
            'No elements with "%s" attribute have been found by "%s" selector.',
263
            $attribute,
264
            $selector
265
        ));
266
    }
267
268
    /**
269
     * @param Scope\BeforeFeatureScope $scope
270
     *   Scope of the processing feature.
271
     *
272
     * @BeforeFeature
273
     */
274
    public static function beforeFeature(Scope\BeforeFeatureScope $scope)
275
    {
276
        self::collectTags($scope->getFeature()->getTags());
277
278
        // Database will be cloned for every feature with @cloneDB tag.
279
        if (self::hasTag('clonedb')) {
280
            self::$database = clone new Database(self::getTag('clonedb', 'default'));
281
        }
282
283
        DrupalKernelPlaceholder::beforeFeature($scope);
284
        DrupalKernelPlaceholder::injectCustomJavascript('CatchErrors');
285
    }
286
287
    /**
288
     * @AfterFeature
289
     */
290
    public static function afterFeature()
291
    {
292
        // Restore initial database when feature is done (call __destruct).
293
        self::$database = null;
294
295
        // Remove injected script.
296
        DrupalKernelPlaceholder::injectCustomJavascript('CatchErrors', true);
297
    }
298
299
    /**
300
     * @param Scope\BeforeScenarioScope $scope
301
     *   Scope of the processing scenario.
302
     *
303
     * @BeforeScenario
304
     */
305
    public function beforeScenario(Scope\BeforeScenarioScope $scope)
306
    {
307
        self::collectTags($scope->getScenario()->getTags());
308
309
        // No need to keep working element between scenarios.
310
        $this->unsetWorkingElement();
311
        // Any page should be visited due to using jQuery and checking the cookies.
312
        $this->getRedirectContext()->visitPage('/');
313
    }
314
315
    /**
316
     * Set the jQuery handlers for "start" and "finish" events of AJAX queries.
317
     * In each method can be used the "waitAjaxAndAnimations" method for check
318
     * that AJAX was finished.
319
     *
320
     * @see RawTqContext::waitAjaxAndAnimations()
321
     *
322
     * @BeforeScenario @javascript
323
     */
324
    public function beforeScenarioJS()
325
    {
326
        $javascript = '';
327
328
        foreach (['Start' => 'true', 'Complete' => 'false'] as $event => $state) {
329
            $javascript .= "$(document).bind('ajax$event', function() {window.__behatAjax = $state;});";
330
        }
331
332
        $this->executeJs($javascript);
333
    }
334
335
    /**
336
     * IMPORTANT! The "BeforeStep" hook should not be tagged, because steps has no tags!
337
     *
338
     * @param Scope\StepScope|Scope\BeforeStepScope $scope
339
     *   Scope of the processing step.
340
     *
341
     * @BeforeStep
342
     */
343
    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...
344
    {
345
        self::$pageUrl = $this->getCurrentUrl();
346
        // To allow Drupal use its internal, web-based functionality, such as "arg()" or "current_path()" etc.
347
        DrupalKernelPlaceholder::setCurrentPath(ltrim(parse_url(static::$pageUrl)['path'], '/'));
348
    }
349
350
    /**
351
     * IMPORTANT! The "AfterStep" hook should not be tagged, because steps has no tags!
352
     *
353
     * @param Scope\StepScope|Scope\AfterStepScope $scope
354
     *   Scope of the processing step.
355
     *
356
     * @AfterStep
357
     */
358
    public function afterStep(Scope\StepScope $scope)
359
    {
360
        // If "mainWindow" variable is not empty that means that additional window has been opened.
361
        // Then, if number of opened windows equals to one, we need to switch back to original window,
362
        // otherwise an error will occur: "Window not found. The browser window may have been closed".
363
        // This happens due to auto closing window by JavaScript (CKFinder does this after choosing a file).
364
        if (!empty($this->mainWindow) && count($this->getWindowNames()) == 1) {
365
            $this->iSwitchToWindow();
366
        }
367
368
        if (self::isStepImpliesJsEvent($scope)) {
369
            $this->waitAjaxAndAnimations();
370
        }
371
    }
372
}
373