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

TqContext::pressElement()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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