Completed
Pull Request — master (#16)
by Sergii
05:09
created

TqContext::checkJavaScriptError()   C

Complexity

Conditions 8
Paths 6

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

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