Completed
Pull Request — master (#16)
by Sergii
07:37
created

TqContext   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 353
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 13

Test Coverage

Coverage 0%

Importance

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