Completed
Push — master ( b800c5...ca996f )
by Sergii
07:04
created

TqContext::assertElementAttribute()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 0
cts 13
cp 0
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 12
nc 3
nop 3
crap 12
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
13
class TqContext extends RawTqContext
14
{
15
    use LogicalAssertion;
16
17
    /**
18
     * The name and working element of main window.
19
     *
20
     * @var array
21
     */
22
    private $mainWindow = [];
23
    /**
24
     * @var Database
25
     */
26
    private static $database;
27
28
    /**
29
     * Supports switching between the two windows only.
30
     *
31
     * @Given /^(?:|I )switch to opened window$/
32
     * @Then /^(?:|I )switch back to main window$/
33
     */
34
    public function iSwitchToWindow()
35
    {
36
        $windows = $this->getWindowNames();
37
38
        // If the window was not switched yet, then store it name and working element for future switching back.
39
        if (empty($this->mainWindow)) {
40
            $this->mainWindow['name'] = array_shift($windows);
41
            $this->mainWindow['element'] = $this->getWorkingElement();
42
43
            $window = reset($windows);
44
        } else {
45
            $window = $this->mainWindow['name'];
46
            $element = $this->mainWindow['element'];
47
48
            $this->mainWindow = [];
49
        }
50
51
        $this->getSession()->switchToWindow($window);
52
        $this->setWorkingElement(isset($element) ? $element : $this->getBodyElement());
53
    }
54
55
    /**
56
     * @Given /^(?:|I )switch to CKFinder window$/
57
     *
58
     * @javascript
59
     */
60
    public function switchToCKFinderWindow()
61
    {
62
        $this->iSwitchToWindow();
63
        $this->executeJsOnElement(
64
            $this->element('css', 'iframe'),
65
            "{{ELEMENT}}.setAttribute('id', 'behat_ckfinder');"
66
        );
67
        $this->iSwitchToAnIframe('behat_ckfinder');
68
    }
69
70
    /**
71
     * @param string $name
72
     *   An iframe name (null for switching back).
73
     *
74
     * @Given /^(?:|I )switch to an iframe "([^"]*)"$/
75
     * @Then /^(?:|I )switch back from an iframe$/
76
     */
77
    public function iSwitchToAnIframe($name = null)
78
    {
79
        $this->getSession()->switchToIFrame($name);
80
    }
81
82
    /**
83
     * Open the page with specified resolution.
84
     *
85
     * @param string $width_height
86
     *   String that satisfy the condition "<WIDTH>x<HEIGHT>".
87
     *
88
     * @example
89
     * Given I should use the "1280x800" resolution
90
     *
91
     * @Given /^(?:|I should )use the "([^"]*)" screen resolution$/
92
     */
93
    public function useScreenResolution($width_height)
94
    {
95
        list($width, $height) = explode('x', $width_height);
96
97
        $this->getSessionDriver()->resizeWindow((int) $width, (int) $height);
98
    }
99
100
    /**
101
     * @param string $action
102
     *   The next actions can be: "press", "click", "double click" and "right click".
103
     * @param string $selector
104
     *   CSS, inaccurate text or selector name from behat.yml can be used.
105
     *
106
     * @throws \WebDriver\Exception\NoSuchElement
107
     *   When element was not found.
108
     *
109
     * @Given /^(?:|I )((?:|(?:double|right) )click|press) on "([^"]*)"$/
110
     */
111
    public function pressElement($action, $selector)
112
    {
113
        // 1. Get the action, divide string by spaces and put it parts into an array.
114
        // 2. Apply the "ucfirst" function for each array element.
115
        // 3. Make string from an array.
116
        // 4. Set the first letter of a string to lower case.
117
        $this->element('*', $selector)->{lcfirst(implode(array_map('ucfirst', explode(' ', $action))))}();
118
    }
119
120
    /**
121
     * @Given /^(?:|I )wait until AJAX is finished$/
122
     *
123
     * @javascript
124
     */
125
    public function waitUntilAjaxIsFinished()
126
    {
127
        $this->waitAjaxAndAnimations();
128
    }
129
130
    /**
131
     * @param string $selector
132
     *   CSS selector or region name.
133
     *
134
     * @Then /^(?:|I )work with elements in "([^"]*)"(?:| region)$/
135
     */
136
    public function workWithElementsInRegion($selector)
137
    {
138
        $this->setWorkingElement($this->element('css', $selector));
139
    }
140
141
    /**
142
     * @Then /^(?:|I )checkout to whole page$/
143
     */
144
    public function unsetWorkingElementScope()
145
    {
146
        $this->unsetWorkingElement();
147
    }
148
149
    /**
150
     * @param int $seconds
151
     *   Amount of seconds when nothing to happens.
152
     *
153
     * @Given /^(?:|I )wait (\d+) seconds$/
154
     */
155
    public function waitSeconds($seconds)
156
    {
157
        sleep($seconds);
158
    }
159
160
    /**
161
     * @param string $selector
162
     *   Text or CSS.
163
     *
164
     * @throws \Exception
165
     *
166
     * @Given /^(?:|I )scroll to "([^"]*)" element$/
167
     *
168
     * @javascript
169
     */
170
    public function scrollToElement($selector)
171
    {
172
        if (!self::hasTag('javascript')) {
173
            throw new \Exception('Scrolling to an element is impossible without a JavaScript.');
174
        }
175
176
        $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...
177
    }
178
179
    /**
180
     * @param string $message
181
     *   JS error.
182
     * @param bool $negate
183
     *   Whether page should or should not contain the error.
184
     * @param string $file
185
     *   File where error appears.
186
     *
187
     * @throws \RuntimeException
188
     * @throws \Exception
189
     *
190
     * @example
191
     * Then check that "TypeError: cell[0] is undefined" JS error appears in "misc/tabledrag.js" file
192
     *
193
     * @Then /^check that "([^"]*)" JS error(| not) appears in "([^"]*)" file$/
194
     *
195
     * @javascript
196
     */
197
    public function checkJavaScriptError($message, $negate, $file)
198
    {
199
        $errors = $this->getSession()->evaluateScript('return JSON.stringify(window.errors);');
200
        $negate = (bool) $negate;
201
202
        if (empty($errors)) {
203
            if (!$negate) {
204
                throw new \RuntimeException('Page does not contain JavaScript errors.');
205
            }
206
        } else {
207
            $base_url = $this->locatePath();
208
209
            foreach (json_decode($errors) as $error) {
210
                $error->location = str_replace($base_url, '', $error->location);
211
212
                switch (static::assertion(
213
                    strpos($error->message, $message) === 0 && strpos($error->location, $file) === 0,
214
                    $negate
215
                )) {
216
                    case 1:
217
                        throw new \Exception(sprintf(
218
                            'The "%s" error found in "%s" file, but should not be.',
219
                            $message,
220
                            $file
221
                        ));
222
223
                    case 2:
224
                        throw new \Exception(sprintf(
225
                            'The "%s" error not found in "%s" file, but should be.',
226
                            $message,
227
                            $file
228
                        ));
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
        $actualValue = $this->element('*', $selector)->getAttribute($attribute);
249
250
        if (null === $actualValue) {
251
            throw new \InvalidArgumentException(sprintf(
252
                'Element does not contain the "%s" attribute.',
253
                $attribute
254
            ));
255
        } elseif ($actualValue !== $expectedValue) {
256
            throw new \Exception(sprintf(
257
                'Attribute "%s" have the "%s" value which is not equal to "%s".',
258
                $attribute,
259
                $actualValue,
260
                $expectedValue
261
            ));
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
        static::setDrupalVariables([
281
            // Set to "false", because the administration menu will not be rendered.
282
            // @see https://www.drupal.org/node/2023625#comment-8607207
283
            'admin_menu_cache_client' => false,
284
        ]);
285
286
        static::injectCustomJavascript('CatchErrors');
287
    }
288
289
    /**
290
     * @AfterFeature
291
     */
292
    public static function afterFeature()
293
    {
294
        // Restore initial database when feature is done (call __destruct).
295
        self::$database = null;
296
297
        // Remove injected script.
298
        static::injectCustomJavascript('CatchErrors', true);
299
    }
300
301
    /**
302
     * @param Scope\BeforeScenarioScope $scope
303
     *   Scope of the processing scenario.
304
     *
305
     * @BeforeScenario
306
     */
307
    public function beforeScenario(Scope\BeforeScenarioScope $scope)
308
    {
309
        self::collectTags($scope->getScenario()->getTags());
310
311
        // No need to keep working element between scenarios.
312
        $this->unsetWorkingElement();
313
        // Any page should be visited due to using jQuery and checking the cookies.
314
        $this->visitPath('/');
315
        // By "Goutte" session we need to visit any page to be able to set a cookie
316
        // for this session and use it for checking request status codes.
317
        $this->visitPath('/', 'goutte');
318
    }
319
320
    /**
321
     * Set the jQuery handlers for "start" and "finish" events of AJAX queries.
322
     * In each method can be used the "waitAjaxAndAnimations" method for check
323
     * that AJAX was finished.
324
     *
325
     * @see RawTqContext::waitAjaxAndAnimations()
326
     *
327
     * @BeforeScenario @javascript
328
     */
329
    public function beforeScenarioJS()
330
    {
331
        $javascript = '';
332
333
        foreach (['Start' => 'true', 'Complete' => 'false'] as $event => $state) {
334
            $javascript .= "$(document).bind('ajax$event', function() {window.__behatAjax = $state;});";
335
        }
336
337
        $this->executeJs($javascript);
338
    }
339
340
    /**
341
     * IMPORTANT! The "BeforeStep" hook should not be tagged, because steps has no tags!
342
     *
343
     * @param Scope\StepScope|Scope\BeforeStepScope $scope
344
     *   Scope of the processing step.
345
     *
346
     * @BeforeStep
347
     */
348
    public function beforeStep(Scope\StepScope $scope)
1 ignored issue
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...
Coding Style introduced by
beforeStep uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
349
    {
350
        self::$pageUrl = $this->getCurrentUrl();
351
        // To allow Drupal use its internal, web-based functionality, such as "arg()" or "current_path()" etc.
352
        $_GET['q'] = ltrim(parse_url(static::$pageUrl)['path'], '/');
353
        drupal_path_initialize();
354
    }
355
356
    /**
357
     * IMPORTANT! The "AfterStep" hook should not be tagged, because steps has no tags!
358
     *
359
     * @param Scope\StepScope|Scope\AfterStepScope $scope
360
     *   Scope of the processing step.
361
     *
362
     * @AfterStep
363
     */
364
    public function afterStep(Scope\StepScope $scope)
365
    {
366
        // If "mainWindow" variable is not empty that means that additional window has been opened.
367
        // Then, if number of opened windows equals to one, we need to switch back to original window,
368
        // otherwise an error will occur: "Window not found. The browser window may have been closed".
369
        // This happens due to auto closing window by JavaScript (CKFinder does this after choosing a file).
370
        if (!empty($this->mainWindow) && count($this->getWindowNames()) == 1) {
371
            $this->iSwitchToWindow();
372
        }
373
374
        if (self::hasTag('javascript') && self::isStepImpliesJsEvent($scope)) {
375
            $this->waitAjaxAndAnimations();
376
        }
377
    }
378
}
379