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

TqContext::beforeFeature()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 0
cts 3
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
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
     * Then check that "TypeError: cell[0] is undefined" JS error appears on the page
200
     *
201
     * @Then /^check that "([^"]*)" JS error(| not) appears (?:in "([^"]*)" file|on the page)$/
202
     *
203
     * @javascript
204
     */
205
    public function checkJavaScriptError($message, $negate, $file = '')
206
    {
207
        $errors = $this->getSession()->evaluateScript('return JSON.stringify(window.errors);');
208
        $negate = (bool) $negate;
209
210
        if (empty($errors)) {
211
            if (!$negate) {
212
                throw new \RuntimeException('Page does not contain JavaScript errors.');
213
            }
214
        } else {
215
            $base_url = $this->locatePath();
216
217
            foreach (json_decode($errors) as $error) {
218
                $error->location = str_replace($base_url, '', $error->location);
219
220
                switch (static::assertion(
221
                    strpos($error->message, $message) === 0 && ('' === $file ?: strpos($error->location, $file) === 0),
222
                    $negate
223
                )) {
224
                    case 1:
225
                        throw new \Exception(sprintf('The "%s" error found, but should not be.', $message));
226
227
                    case 2:
228
                        throw new \Exception(sprintf('The "%s" error not found, but should be.', $message));
229
                }
230
            }
231
        }
232
    }
233
234
    /**
235
     * @param string $selector
236
     * @param string $attribute
237
     * @param string $expectedValue
238
     *
239
     * @throws \Exception
240
     *
241
     * @example
242
     * Then I should see the "#table_cell" element with "colspan" attribute having "3" value
243
     *
244
     * @Then /^(?:|I )should see the "([^"]*)" element with "([^"]*)" attribute having "([^"]*)" value$/
245
     */
246
    public function assertElementAttribute($selector, $attribute, $expectedValue)
247
    {
248
        foreach ($this->findAll($selector) as $element) {
249
            if ($element->getAttribute($attribute) === $expectedValue) {
250
                return;
251
            }
252
        }
253
254
        throw new \InvalidArgumentException(sprintf(
255
            'No elements with "%s" attribute have been found by "%s" selector.',
256
            $attribute,
257
            $selector
258
        ));
259
    }
260
261
    /**
262
     * @param Scope\BeforeFeatureScope $scope
263
     *   Scope of the processing feature.
264
     *
265
     * @BeforeFeature
266
     */
267
    public static function beforeFeature(Scope\BeforeFeatureScope $scope)
268
    {
269
        self::collectTags($scope->getFeature()->getTags());
270
271
        // Database will be cloned for every feature with @cloneDB tag.
272
        if (self::hasTag('clonedb')) {
273
            self::$database = clone new Database(self::getTag('clonedb', 'default'));
274
        }
275
276
        DrupalKernelPlaceholder::beforeFeature($scope);
277
        DrupalKernelPlaceholder::injectCustomJavascript('CatchErrors');
278
    }
279
280
    /**
281
     * @AfterFeature
282
     */
283
    public static function afterFeature()
284
    {
285
        // Restore initial database when feature is done (call __destruct).
286
        self::$database = null;
287
288
        // Remove injected script.
289
        DrupalKernelPlaceholder::injectCustomJavascript('CatchErrors', true);
290
    }
291
292
    /**
293
     * @param Scope\BeforeScenarioScope $scope
294
     *   Scope of the processing scenario.
295
     *
296
     * @BeforeScenario
297
     */
298
    public function beforeScenario(Scope\BeforeScenarioScope $scope)
299
    {
300
        self::collectTags($scope->getScenario()->getTags());
301
302
        // No need to keep working element between scenarios.
303
        $this->unsetWorkingElement();
304
        // Any page should be visited due to using jQuery and checking the cookies.
305
        $this->getRedirectContext()->visitPage('/');
306
    }
307
308
    /**
309
     * Set the jQuery handlers for "start" and "finish" events of AJAX queries.
310
     * In each method can be used the "waitAjaxAndAnimations" method for check
311
     * that AJAX was finished.
312
     *
313
     * @see RawTqContext::waitAjaxAndAnimations()
314
     *
315
     * @BeforeScenario @javascript
316
     */
317
    public function beforeScenarioJS()
318
    {
319
        $javascript = '';
320
321
        foreach (['Start' => 'true', 'Complete' => 'false'] as $event => $state) {
322
            $javascript .= "$(document).bind('ajax$event', function() {window.__behatAjax = $state;});";
323
        }
324
325
        $this->executeJs($javascript);
326
    }
327
328
    /**
329
     * IMPORTANT! The "BeforeStep" hook should not be tagged, because steps has no tags!
330
     *
331
     * @param Scope\StepScope|Scope\BeforeStepScope $scope
332
     *   Scope of the processing step.
333
     *
334
     * @BeforeStep
335
     */
336
    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...
337
    {
338
        self::$pageUrl = $this->getCurrentUrl();
339
        // To allow Drupal use its internal, web-based functionality, such as "arg()" or "current_path()" etc.
340
        DrupalKernelPlaceholder::setCurrentPath(ltrim(parse_url(self::$pageUrl)['path'], '/'));
341
    }
342
343
    /**
344
     * IMPORTANT! The "AfterStep" hook should not be tagged, because steps has no tags!
345
     *
346
     * @param Scope\StepScope|Scope\AfterStepScope $scope
347
     *   Scope of the processing step.
348
     *
349
     * @AfterStep
350
     */
351
    public function afterStep(Scope\StepScope $scope)
352
    {
353
        // If "mainWindow" variable is not empty that means that additional window has been opened.
354
        // Then, if number of opened windows equals to one, we need to switch back to original window,
355
        // otherwise an error will occur: "Window not found. The browser window may have been closed".
356
        // This happens due to auto closing window by JavaScript (CKFinder does this after choosing a file).
357
        if (!empty($this->mainWindow) && count($this->getWindowNames()) == 1) {
358
            $this->iSwitchToWindow();
359
        }
360
361
        if (self::isStepImpliesJsEvent($scope)) {
362
            $this->waitAjaxAndAnimations();
363
        }
364
    }
365
}
366