behat_general.php 85.7 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * General use steps definitions.
 *
 * @package   core
 * @category  test
 * @copyright 2012 David Monllaó
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.

require_once(__DIR__ . '/../../behat/behat_base.php');

30
31
32
33
34
35
use Behat\Gherkin\Node\TableNode as TableNode;
use Behat\Mink\Exception\DriverException as DriverException;
use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
use Behat\Mink\Exception\ExpectationException as ExpectationException;
use WebDriver\Exception\NoSuchElement as NoSuchElement;
use WebDriver\Exception\StaleElementReference as StaleElementReference;
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

/**
 * Cross component steps definitions.
 *
 * Basic web application definitions from MinkExtension and
 * BehatchExtension. Definitions modified according to our needs
 * when necessary and including only the ones we need to avoid
 * overlapping and confusion.
 *
 * @package   core
 * @category  test
 * @copyright 2012 David Monllaó
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class behat_general extends behat_base {

52
53
54
55
56
57
    /**
     * @var string used by {@link switch_to_window()} and
     * {@link switch_to_the_main_window()} to work-around a Chrome browser issue.
     */
    const MAIN_WINDOW_NAME = '__moodle_behat_main_window_name';

58
59
60
61
62
63
64
    /**
     * @var string when we want to check whether or not a new page has loaded,
     * we first write this unique string into the page. Then later, by checking
     * whether it is still there, we can tell if a new page has been loaded.
     */
    const PAGE_LOAD_DETECTION_STRING = 'new_page_not_loaded_since_behat_started_watching';

65
66
67
68
    /**
     * @var $pageloaddetectionrunning boolean Used to ensure that page load detection was started before a page reload
     * was checked for.
     */
69
    private $pageloaddetectionrunning = false;
70

71
72
73
74
75
76
    /**
     * Opens Moodle homepage.
     *
     * @Given /^I am on homepage$/
     */
    public function i_am_on_homepage() {
77
        $this->execute('behat_general::i_visit', ['/']);
78
79
    }

80
81
82
83
84
85
    /**
     * Opens Moodle site homepage.
     *
     * @Given /^I am on site homepage$/
     */
    public function i_am_on_site_homepage() {
86
        $this->execute('behat_general::i_visit', ['/?redirect=0']);
87
88
    }

89
90
91
92
93
94
    /**
     * Opens course index page.
     *
     * @Given /^I am on course index$/
     */
    public function i_am_on_course_index() {
95
        $this->execute('behat_general::i_visit', ['/course/index.php']);
96
97
    }

98
99
100
101
102
103
104
105
106
    /**
     * Reloads the current page.
     *
     * @Given /^I reload the page$/
     */
    public function reload() {
        $this->getSession()->reload();
    }

107
108
109
110
111
112
113
114
115
116
117
    /**
     * Follows the page redirection. Use this step after any action that shows a message and waits for a redirection
     *
     * @Given /^I wait to be redirected$/
     */
    public function i_wait_to_be_redirected() {

        // Xpath and processes based on core_renderer::redirect_message(), core_renderer::$metarefreshtag and
        // moodle_page::$periodicrefreshdelay possible values.
        if (!$metarefresh = $this->getSession()->getPage()->find('xpath', "//head/descendant::meta[@http-equiv='refresh']")) {
            // We don't fail the scenario if no redirection with message is found to avoid race condition false failures.
118
            return true;
119
120
        }

121
122
123
124
        // Wrapped in try & catch in case the redirection has already been executed.
        try {
            $content = $metarefresh->getAttribute('content');
        } catch (NoSuchElement $e) {
125
            return true;
126
        } catch (StaleElementReference $e) {
127
            return true;
128
129
130
        }

        // Getting the refresh time and the url if present.
131
132
        if (strstr($content, 'url') != false) {

133
            list($waittime, $url) = explode(';', $content);
134
135
136
137
138
139
140
141
142
143
144
145

            // Cleaning the URL value.
            $url = trim(substr($url, strpos($url, 'http')));

        } else {
            // Just wait then.
            $waittime = $content;
        }


        // Wait until the URL change is executed.
        if ($this->running_javascript()) {
146
            $this->getSession()->wait($waittime * 1000);
147
148
149
150
151
152
153
154
155
156
157

        } else if (!empty($url)) {
            // We redirect directly as we can not wait for an automatic redirection.
            $this->getSession()->getDriver()->getClient()->request('get', $url);

        } else {
            // Reload the page if no URL was provided.
            $this->getSession()->getDriver()->reload();
        }
    }

158
159
160
161
    /**
     * Switches to the specified iframe.
     *
     * @Given /^I switch to "(?P<iframe_name_string>(?:[^"]|\\")*)" iframe$/
162
     * @Given /^I switch to "(?P<iframe_name_string>(?:[^"]|\\")*)" class iframe$/
163
     * @param string $name The name of the iframe
164
     */
165
    public function switch_to_iframe($name) {
166
167
168
169
        // We spin to give time to the iframe to be loaded.
        // Using extended timeout as we don't know about which
        // kind of iframe will be loaded.
        $this->spin(
170
171
172
            function($context) use ($name){
                $iframe = $context->find('iframe', $name);
                if ($iframe->hasAttribute('name')) {
173
                    $iframename = $iframe->getAttribute('name');
174
175
176
177
178
179
                } else {
                    if (!$this->running_javascript()) {
                        throw new \coding_exception('iframe must have a name attribute to use the switchTo command.');
                    }
                    $iframename = uniqid();
                    $this->execute_js_on_node($iframe, "{{ELEMENT}}.name = '{$iframename}';");
180
181
182
183
184
185
186
187
188
189
                }
                $context->getSession()->switchToIFrame($iframename);

                // If no exception we are done.
                return true;
            },
            behat_base::get_extended_timeout()
        );
    }

190
191
192
193
194
195
196
197
198
    /**
     * Switches to the main Moodle frame.
     *
     * @Given /^I switch to the main frame$/
     */
    public function switch_to_the_main_frame() {
        $this->getSession()->switchToIFrame();
    }

199
200
201
202
203
204
205
    /**
     * Switches to the specified window. Useful when interacting with popup windows.
     *
     * @Given /^I switch to "(?P<window_name_string>(?:[^"]|\\")*)" window$/
     * @param string $windowname
     */
    public function switch_to_window($windowname) {
206
207
208
209
210
        if ($windowname === self::MAIN_WINDOW_NAME) {
            // When switching to the main window normalise the window name to null.
            // This is normalised further in the Mink driver to the root window ID.
            $windowname = null;
        }
211

212
213
214
215
216
217
218
219
220
        $this->getSession()->switchToWindow($windowname);
    }

    /**
     * Switches to the main Moodle window. Useful when you finish interacting with popup windows.
     *
     * @Given /^I switch to the main window$/
     */
    public function switch_to_the_main_window() {
221
        $this->switch_to_window(self::MAIN_WINDOW_NAME);
222
223
    }

224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
    /**
     * Closes all extra windows opened during the navigation.
     *
     * This assumes all popups are opened by the main tab and you will now get back.
     *
     * @Given /^I close all opened windows$/
     * @throws DriverException If there aren't exactly 1 tabs open when finish or no javascript running
     */
    public function i_close_all_opened_windows() {
        if (!$this->running_javascript()) {
            throw new DriverException('Closing windows steps require javascript');
        }
        $names = $this->getSession()->getWindowNames();
        for ($index = 1; $index < count($names); $index ++) {
            $this->getSession()->switchToWindow($names[$index]);
239
            $this->execute_script("window.open('', '_self').close();");
240
241
242
243
244
245
246
247
        }
        $names = $this->getSession()->getWindowNames();
        if (count($names) !== 1) {
            throw new DriverException('Expected to see 1 tabs open, not ' . count($names));
        }
        $this->getSession()->switchToWindow($names[0]);
    }

248
    /**
249
     * Accepts the currently displayed alert dialog. This step does not work in all the browsers, consider it experimental.
250
251
252
     * @Given /^I accept the currently displayed dialog$/
     */
    public function accept_currently_displayed_alert_dialog() {
253
        $this->getSession()->getDriver()->getWebDriver()->switchTo()->alert()->accept();
254
255
    }

256
257
258
259
260
    /**
     * Dismisses the currently displayed alert dialog. This step does not work in all the browsers, consider it experimental.
     * @Given /^I dismiss the currently displayed dialog$/
     */
    public function dismiss_currently_displayed_alert_dialog() {
261
        $this->getSession()->getDriver()->getWebDriver()->switchTo()->alert()->dismiss();
262
263
    }

264
265
266
267
    /**
     * Clicks link with specified id|title|alt|text.
     *
     * @When /^I follow "(?P<link_string>(?:[^"]|\\")*)"$/
268
     * @throws ElementNotFoundException Thrown by behat_base::find
269
     * @param string $link
270
271
     */
    public function click_link($link) {
272
273

        $linknode = $this->find_link($link);
274
        $this->ensure_node_is_visible($linknode);
275
        $linknode->click();
276
277
278
279
280
281
282
283
284
    }

    /**
     * Waits X seconds. Required after an action that requires data from an AJAX request.
     *
     * @Then /^I wait "(?P<seconds_number>\d+)" seconds$/
     * @param int $seconds
     */
    public function i_wait_seconds($seconds) {
285
        if ($this->running_javascript()) {
286
            $this->getSession()->wait($seconds * 1000);
287
288
        } else {
            sleep($seconds);
289
        }
290
291
292
293
294
295
296
297
    }

    /**
     * Waits until the page is completely loaded. This step is auto-executed after every step.
     *
     * @Given /^I wait until the page is ready$/
     */
    public function wait_until_the_page_is_ready() {
298

299
        // No need to wait if not running JS.
300
        if (!$this->running_javascript()) {
301
            return;
302
303
        }

304
        $this->getSession()->wait(self::get_timeout() * 1000, self::PAGE_READY_JS);
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
    }

    /**
     * Waits until the provided element selector exists in the DOM
     *
     * Using the protected method as this method will be usually
     * called by other methods which are not returning a set of
     * steps and performs the actions directly, so it would not
     * be executed if it returns another step.

     * @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" exists$/
     * @param string $element
     * @param string $selector
     * @return void
     */
    public function wait_until_exists($element, $selectortype) {
        $this->ensure_element_exists($element, $selectortype);
    }

    /**
     * Waits until the provided element does not exist in the DOM
     *
     * Using the protected method as this method will be usually
     * called by other methods which are not returning a set of
     * steps and performs the actions directly, so it would not
     * be executed if it returns another step.

     * @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" does not exist$/
     * @param string $element
     * @param string $selector
     * @return void
     */
    public function wait_until_does_not_exists($element, $selectortype) {
        $this->ensure_element_does_not_exist($element, $selectortype);
339
340
341
    }

    /**
342
     * Generic mouse over action. Mouse over a element of the specified type.
343
     *
344
345
346
     * @When /^I hover "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
     * @param string $element Element we look for
     * @param string $selectortype The type of what we look for
347
     */
348
349
350
    public function i_hover($element, $selectortype) {
        // Gets the node based on the requested selector type and locator.
        $node = $this->get_selected_node($selectortype, $element);
351
        $this->execute_js_on_node($node, '{{ELEMENT}}.scrollIntoView();');
352
353
354
        $node->mouseOver();
    }

355
356
357
358
359
360
361
362
363
364
365
    /**
     * Generic click action. Click on the element of the specified type.
     *
     * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
     * @param string $element Element we look for
     * @param string $selectortype The type of what we look for
     */
    public function i_click_on($element, $selectortype) {

        // Gets the node based on the requested selector type and locator.
        $node = $this->get_selected_node($selectortype, $element);
366
        $this->ensure_node_is_visible($node);
367
368
369
        $node->click();
    }

370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
    /**
     * Sets the focus and takes away the focus from an element, generating blur JS event.
     *
     * @When /^I take focus off "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
     * @param string $element Element we look for
     * @param string $selectortype The type of what we look for
     */
    public function i_take_focus_off_field($element, $selectortype) {
        if (!$this->running_javascript()) {
            throw new ExpectationException('Can\'t take focus off from "' . $element . '" in non-js mode', $this->getSession());
        }
        // Gets the node based on the requested selector type and locator.
        $node = $this->get_selected_node($selectortype, $element);
        $this->ensure_node_is_visible($node);

        // Ensure element is focused before taking it off.
        $node->focus();
        $node->blur();
    }

390
391
392
393
394
    /**
     * Clicks the specified element and confirms the expected dialogue.
     *
     * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" confirming the dialogue$/
     * @throws ElementNotFoundException Thrown by behat_base::find
395
396
     * @param string $element Element we look for
     * @param string $selectortype The type of what we look for
397
398
399
     */
    public function i_click_on_confirming_the_dialogue($element, $selectortype) {
        $this->i_click_on($element, $selectortype);
400
401
        $this->execute('behat_general::accept_currently_displayed_alert_dialog', []);
        $this->wait_until_the_page_is_ready();
402
403
    }

404
405
406
407
408
    /**
     * Clicks the specified element and dismissing the expected dialogue.
     *
     * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" dismissing the dialogue$/
     * @throws ElementNotFoundException Thrown by behat_base::find
409
410
     * @param string $element Element we look for
     * @param string $selectortype The type of what we look for
411
412
413
     */
    public function i_click_on_dismissing_the_dialogue($element, $selectortype) {
        $this->i_click_on($element, $selectortype);
414
415
        $this->execute('behat_general::dismiss_currently_displayed_alert_dialog', []);
        $this->wait_until_the_page_is_ready();
416
417
    }

418
419
420
421
422
423
424
425
426
427
428
429
    /**
     * Click on the element of the specified type which is located inside the second element.
     *
     * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
     * @param string $element Element we look for
     * @param string $selectortype The type of what we look for
     * @param string $nodeelement Element we look in
     * @param string $nodeselectortype The type of selector where we look in
     */
    public function i_click_on_in_the($element, $selectortype, $nodeelement, $nodeselectortype) {

        $node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
430
        $this->ensure_node_is_visible($node);
431
432
433
        $node->click();
    }

434
    /**
435
     * Drags and drops the specified element to the specified container. This step does not work in all the browsers, consider it experimental.
436
437
438
439
440
441
442
443
444
445
446
     *
     * The steps definitions calling this step as part of them should
     * manage the wait times by themselves as the times and when the
     * waits should be done depends on what is being dragged & dropper.
     *
     * @Given /^I drag "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" and I drop it in "(?P<container_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
     * @param string $element
     * @param string $selectortype
     * @param string $containerelement
     * @param string $containerselectortype
     */
447
448
449
450
    public function i_drag_and_i_drop_it_in($source, $sourcetype, $target, $targettype) {
        if (!$this->running_javascript()) {
            throw new DriverException('Drag and drop steps require javascript');
        }
451

452
453
        $source = $this->find($sourcetype, $source);
        $target = $this->find($targettype, $target);
454

455
456
        if (!$source->isVisible()) {
            throw new ExpectationException("'{$source}' '{$sourcetype}' is not visible", $this->getSession());
457
        }
458
459
        if (!$target->isVisible()) {
            throw new ExpectationException("'{$target}' '{$targettype}' is not visible", $this->getSession());
460
461
        }

462
        $this->getSession()->getDriver()->dragTo($source->getXpath(), $target->getXpath());
463
464
    }

465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
    /**
     * Checks, that the specified element is visible. Only available in tests using Javascript.
     *
     * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" should be visible$/
     * @throws ElementNotFoundException
     * @throws ExpectationException
     * @throws DriverException
     * @param string $element
     * @param string $selectortype
     * @return void
     */
    public function should_be_visible($element, $selectortype) {

        if (!$this->running_javascript()) {
            throw new DriverException('Visible checks are disabled in scenarios without Javascript support');
        }

        $node = $this->get_selected_node($selectortype, $element);
        if (!$node->isVisible()) {
            throw new ExpectationException('"' . $element . '" "' . $selectortype . '" is not visible', $this->getSession());
        }
    }

    /**
489
     * Checks, that the existing element is not visible. Only available in tests using Javascript.
490
     *
491
492
493
494
     * As a "not" method, it's performance could not be good, but in this
     * case the performance is good because the element must exist,
     * otherwise there would be a ElementNotFoundException, also here we are
     * not spinning until the element is visible.
495
     *
496
497
498
499
500
501
502
503
504
505
506
507
508
     * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" should not be visible$/
     * @throws ElementNotFoundException
     * @throws ExpectationException
     * @param string $element
     * @param string $selectortype
     * @return void
     */
    public function should_not_be_visible($element, $selectortype) {

        try {
            $this->should_be_visible($element, $selectortype);
        } catch (ExpectationException $e) {
            // All as expected.
509
            return;
510
        }
511
        throw new ExpectationException('"' . $element . '" "' . $selectortype . '" is visible', $this->getSession());
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
    }

    /**
     * Checks, that the specified element is visible inside the specified container. Only available in tests using Javascript.
     *
     * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" should be visible$/
     * @throws ElementNotFoundException
     * @throws DriverException
     * @throws ExpectationException
     * @param string $element Element we look for
     * @param string $selectortype The type of what we look for
     * @param string $nodeelement Element we look in
     * @param string $nodeselectortype The type of selector where we look in
     */
    public function in_the_should_be_visible($element, $selectortype, $nodeelement, $nodeselectortype) {

        if (!$this->running_javascript()) {
            throw new DriverException('Visible checks are disabled in scenarios without Javascript support');
        }

        $node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
        if (!$node->isVisible()) {
            throw new ExpectationException(
                '"' . $element . '" "' . $selectortype . '" in the "' . $nodeelement . '" "' . $nodeselectortype . '" is not visible',
                $this->getSession()
            );
        }
    }

    /**
542
543
544
545
546
547
     * Checks, that the existing element is not visible inside the existing container. Only available in tests using Javascript.
     *
     * As a "not" method, it's performance could not be good, but in this
     * case the performance is good because the element must exist,
     * otherwise there would be a ElementNotFoundException, also here we are
     * not spinning until the element is visible.
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
     *
     * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" should not be visible$/
     * @throws ElementNotFoundException
     * @throws ExpectationException
     * @param string $element Element we look for
     * @param string $selectortype The type of what we look for
     * @param string $nodeelement Element we look in
     * @param string $nodeselectortype The type of selector where we look in
     */
    public function in_the_should_not_be_visible($element, $selectortype, $nodeelement, $nodeselectortype) {

        try {
            $this->in_the_should_be_visible($element, $selectortype, $nodeelement, $nodeselectortype);
        } catch (ExpectationException $e) {
            // All as expected.
563
            return;
564
        }
565
566
567
568
        throw new ExpectationException(
            '"' . $element . '" "' . $selectortype . '" in the "' . $nodeelement . '" "' . $nodeselectortype . '" is visible',
            $this->getSession()
        );
569
570
    }

571
    /**
572
     * Checks, that page contains specified text. It also checks if the text is visible when running Javascript tests.
573
574
     *
     * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)"$/
575
     * @throws ExpectationException
576
     * @param string $text
577
578
     */
    public function assert_page_contains_text($text) {
579

580
581
        // Looking for all the matching nodes without any other descendant matching the
        // same xpath (we are using contains(., ....).
582
        $xpathliteral = behat_context_helper::escape($text);
583
584
        $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
            "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
585
586

        try {
587
            $nodes = $this->find_all('xpath', $xpath);
588
589
590
        } catch (ElementNotFoundException $e) {
            throw new ExpectationException('"' . $text . '" text was not found in the page', $this->getSession());
        }
591

592
593
594
595
596
597
598
        // If we are not running javascript we have enough with the
        // element existing as we can't check if it is visible.
        if (!$this->running_javascript()) {
            return;
        }

        // We spin as we don't have enough checking that the element is there, we
599
600
        // should also ensure that the element is visible. Using microsleep as this
        // is a repeated step and global performance is important.
601
602
603
604
        $this->spin(
            function($context, $args) {

                foreach ($args['nodes'] as $node) {
605
                    if ($node->isVisible()) {
606
                        return true;
607
608
609
                    }
                }

610
611
612
                // If non of the nodes is visible we loop again.
                throw new ExpectationException('"' . $args['text'] . '" text was found but was not visible', $context->getSession());
            },
613
614
615
616
            array('nodes' => $nodes, 'text' => $text),
            false,
            false,
            true
617
        );
618

619
620
621
    }

    /**
622
     * Checks, that page doesn't contain specified text. When running Javascript tests it also considers that texts may be hidden.
623
624
     *
     * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)"$/
625
     * @throws ExpectationException
626
     * @param string $text
627
628
     */
    public function assert_page_not_contains_text($text) {
629

630
631
        // Looking for all the matching nodes without any other descendant matching the
        // same xpath (we are using contains(., ....).
632
        $xpathliteral = behat_context_helper::escape($text);
633
634
635
636
        $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
            "[count(descendant::*[contains(., $xpathliteral)]) = 0]";

        // We should wait a while to ensure that the page is not still loading elements.
637
        // Waiting less than self::get_timeout() as we already waited for the DOM to be ready and
638
        // all JS to be executed.
639
        try {
640
            $nodes = $this->find_all('xpath', $xpath, false, false, self::get_reduced_timeout());
641
642
        } catch (ElementNotFoundException $e) {
            // All ok.
643
            return;
644
        }
645

646
647
648
649
650
651
652
653
654
655
656
        // If we are not running javascript we have enough with the
        // element existing as we can't check if it is hidden.
        if (!$this->running_javascript()) {
            throw new ExpectationException('"' . $text . '" text was found in the page', $this->getSession());
        }

        // If the element is there we should be sure that it is not visible.
        $this->spin(
            function($context, $args) {

                foreach ($args['nodes'] as $node) {
657
658
659
660
661
662
663
664
665
666
                    // If element is removed from dom, then just exit.
                    try {
                        // If element is visible then throw exception, so we keep spinning.
                        if ($node->isVisible()) {
                            throw new ExpectationException('"' . $args['text'] . '" text was found in the page',
                                $context->getSession());
                        }
                    } catch (WebDriver\Exception\NoSuchElement $e) {
                        // Do nothing just return, as element is no more on page.
                        return true;
667
668
669
                    } catch (ElementNotFoundException $e) {
                        // Do nothing just return, as element is no more on page.
                        return true;
670
671
672
673
674
675
                    }
                }

                // If non of the found nodes is visible we consider that the text is not visible.
                return true;
            },
676
            array('nodes' => $nodes, 'text' => $text),
677
            behat_base::get_reduced_timeout(),
678
679
            false,
            true
680
        );
681
682
683
    }

    /**
684
     * Checks, that the specified element contains the specified text. When running Javascript tests it also considers that texts may be hidden.
685
     *
686
     * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
687
688
     * @throws ElementNotFoundException
     * @throws ExpectationException
689
690
691
     * @param string $text
     * @param string $element Element we look in.
     * @param string $selectortype The type of element where we are looking in.
692
     */
693
694
    public function assert_element_contains_text($text, $element, $selectortype) {

695
696
697
        // Getting the container where the text should be found.
        $container = $this->get_selected_node($selectortype, $element);

698
699
        // Looking for all the matching nodes without any other descendant matching the
        // same xpath (we are using contains(., ....).
700
        $xpathliteral = behat_context_helper::escape($text);
701
702
        $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
            "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
703
704
705

        // Wait until it finds the text inside the container, otherwise custom exception.
        try {
706
            $nodes = $this->find_all('xpath', $xpath, false, $container);
707
708
709
        } catch (ElementNotFoundException $e) {
            throw new ExpectationException('"' . $text . '" text was not found in the "' . $element . '" element', $this->getSession());
        }
710

711
712
713
714
715
716
        // If we are not running javascript we have enough with the
        // element existing as we can't check if it is visible.
        if (!$this->running_javascript()) {
            return;
        }

717
718
        // We also check the element visibility when running JS tests. Using microsleep as this
        // is a repeated step and global performance is important.
719
720
721
722
        $this->spin(
            function($context, $args) {

                foreach ($args['nodes'] as $node) {
723
                    if ($node->isVisible()) {
724
                        return true;
725
726
727
                    }
                }

728
729
                throw new ExpectationException('"' . $args['text'] . '" text was found in the "' . $args['element'] . '" element but was not visible', $context->getSession());
            },
730
731
732
733
            array('nodes' => $nodes, 'text' => $text, 'element' => $element),
            false,
            false,
            true
734
        );
735
736
737
    }

    /**
738
     * Checks, that the specified element does not contain the specified text. When running Javascript tests it also considers that texts may be hidden.
739
     *
740
     * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
741
742
     * @throws ElementNotFoundException
     * @throws ExpectationException
743
744
745
     * @param string $text
     * @param string $element Element we look in.
     * @param string $selectortype The type of element where we are looking in.
746
     */
747
748
    public function assert_element_not_contains_text($text, $element, $selectortype) {

749
750
751
752
753
        // Getting the container where the text should be found.
        $container = $this->get_selected_node($selectortype, $element);

        // Looking for all the matching nodes without any other descendant matching the
        // same xpath (we are using contains(., ....).
754
        $xpathliteral = behat_context_helper::escape($text);
755
756
757
758
759
        $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
            "[count(descendant::*[contains(., $xpathliteral)]) = 0]";

        // We should wait a while to ensure that the page is not still loading elements.
        // Giving preference to the reliability of the results rather than to the performance.
760
        try {
761
            $nodes = $this->find_all('xpath', $xpath, false, $container, self::get_reduced_timeout());
762
763
        } catch (ElementNotFoundException $e) {
            // All ok.
764
765
766
            return;
        }

767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
        // If we are not running javascript we have enough with the
        // element not being found as we can't check if it is visible.
        if (!$this->running_javascript()) {
            throw new ExpectationException('"' . $text . '" text was found in the "' . $element . '" element', $this->getSession());
        }

        // We need to ensure all the found nodes are hidden.
        $this->spin(
            function($context, $args) {

                foreach ($args['nodes'] as $node) {
                    if ($node->isVisible()) {
                        throw new ExpectationException('"' . $args['text'] . '" text was found in the "' . $args['element'] . '" element', $context->getSession());
                    }
                }

                // If all the found nodes are hidden we are happy.
                return true;
            },
786
            array('nodes' => $nodes, 'text' => $text, 'element' => $element),
787
            behat_base::get_reduced_timeout(),
788
789
            false,
            true
790
        );
791
792
    }

793
794
795
    /**
     * Checks, that the first specified element appears before the second one.
     *
796
     * @Then :preelement :preselectortype should appear before :postelement :postselectortype
797
     * @Then :preelement :preselectortype should appear before :postelement :postselectortype in the :containerelement :containerselectortype
798
799
     * @throws ExpectationException
     * @param string $preelement The locator of the preceding element
800
     * @param string $preselectortype The selector type of the preceding element
801
802
     * @param string $postelement The locator of the latest element
     * @param string $postselectortype The selector type of the latest element
803
804
     * @param string $containerelement
     * @param string $containerselectortype
805
     */
806
807
808
809
    public function should_appear_before(
        string $preelement,
        string $preselectortype,
        string $postelement,
810
811
812
        string $postselectortype,
        ?string $containerelement = null,
        ?string $containerselectortype = null
813
    ) {
814
        $msg = "'{$preelement}' '{$preselectortype}' does not appear before '{$postelement}' '{$postselectortype}'";
815
        $this->check_element_order(
816
817
            $containerelement,
            $containerselectortype,
818
819
820
821
822
823
            $preelement,
            $preselectortype,
            $postelement,
            $postselectortype,
            $msg
        );
824
825
826
827
828
    }

    /**
     * Checks, that the first specified element appears after the second one.
     *
829
     * @Then :postelement :postselectortype should appear after :preelement :preselectortype
830
     * @Then :postelement :postselectortype should appear after :preelement :preselectortype in the :containerelement :containerselectortype
831
832
833
834
     * @throws ExpectationException
     * @param string $postelement The locator of the latest element
     * @param string $postselectortype The selector type of the latest element
     * @param string $preelement The locator of the preceding element
835
     * @param string $preselectortype The selector type of the preceding element
836
837
     * @param string $containerelement
     * @param string $containerselectortype
838
     */
839
840
841
842
    public function should_appear_after(
        string $postelement,
        string $postselectortype,
        string $preelement,
843
844
845
        string $preselectortype,
        ?string $containerelement = null,
        ?string $containerselectortype = null
846
847
848
    ) {
        $msg = "'{$postelement}' '{$postselectortype}' does not appear after '{$preelement}' '{$preselectortype}'";
        $this->check_element_order(
849
850
            $containerelement,
            $containerselectortype,
851
852
853
854
855
856
            $preelement,
            $preselectortype,
            $postelement,
            $postselectortype,
            $msg
        );
857
    }
858

859
860
861
    /**
     * Shared code to check whether an element is before or after another one.
     *
862
863
     * @param string $containerelement
     * @param string $containerselectortype
864
865
866
867
868
869
     * @param string $preelement The locator of the preceding element
     * @param string $preselectortype The locator of the preceding element
     * @param string $postelement The locator of the following element
     * @param string $postselectortype The selector type of the following element
     * @param string $msg Message to output if this fails
     */
870
    protected function check_element_order(
871
872
        ?string $containerelement,
        ?string $containerselectortype,
873
874
875
876
877
878
        string $preelement,
        string $preselectortype,
        string $postelement,
        string $postselectortype,
        string $msg
    ) {
879
880
881
882
883
884
885
        $containernode = false;
        if ($containerselectortype && $containerelement) {
            // Get the container node.
            $containernode = $this->get_selected_node($containerselectortype, $containerelement);
            $msg .= " in the '{$containerelement}' '{$containerselectortype}'";
        }

886
        list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement);
887
        list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
888

889
890
891
892
893
894
895
        $newlines = [
            "\r\n",
            "\r",
            "\n",
        ];
        $prexpath = str_replace($newlines, ' ', $this->find($preselector, $prelocator, false, $containernode)->getXpath());
        $postxpath = str_replace($newlines, ' ', $this->find($postselector, $postlocator, false, $containernode)->getXpath());
896

897
898
899
900
901
        if ($this->running_javascript()) {
            // The xpath to do this was running really slowly on certain Chrome versions so we are using
            // this DOM method instead.
            $js = <<<EOF
(function() {
902
903
    var a = document.evaluate("{$prexpath}", document, null, XPathResult.ANY_TYPE, null).iterateNext();
    var b = document.evaluate("{$postxpath}", document, null, XPathResult.ANY_TYPE, null).iterateNext();
904
905
906
    return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING;
})()
EOF;
907
            $ok = $this->evaluate_script($js);
908
909
910
911
912
913
        } else {

            // Using following xpath axe to find it.
            $xpath = "{$prexpath}/following::*[contains(., {$postxpath})]";
            $ok = $this->getSession()->getDriver()->find($xpath);
        }
914

915
        if (!$ok) {
916
917
918
919
            throw new ExpectationException($msg, $this->getSession());
        }
    }

920
    /**
921
     * Checks, that element of specified type is disabled.
922
     *
923
     * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be disabled$/
924
     * @throws ExpectationException Thrown by behat_base::find
925
926
     * @param string $element Element we look in
     * @param string $selectortype The type of element where we are looking in.
927
     */
928
    public function the_element_should_be_disabled($element, $selectortype) {
929
        $this->the_attribute_of_should_be_set("disabled", $element, $selectortype, false);
930
931
932
    }

    /**
933
     * Checks, that element of specified type is enabled.
934
     *
935
     * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be enabled$/
936
     * @throws ExpectationException Thrown by behat_base::find
937
938
     * @param string $element Element we look on
     * @param string $selectortype The type of where we look
939
     */
940
    public function the_element_should_be_enabled($element, $selectortype) {
941
        $this->the_attribute_of_should_be_set("disabled", $element, $selectortype, true);
942
943
    }

944
945
946
947
948
949
950
951
952
    /**
     * Checks the provided element and selector type are readonly on the current page.
     *
     * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be readonly$/
     * @throws ExpectationException Thrown by behat_base::find
     * @param string $element Element we look in
     * @param string $selectortype The type of element where we are looking in.
     */
    public function the_element_should_be_readonly($element, $selectortype) {
953
        $this->the_attribute_of_should_be_set("readonly", $element, $selectortype, false);
954
955
956
957
958
959
960
961
962
963
964
    }

    /**
     * Checks the provided element and selector type are not readonly on the current page.
     *
     * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not be readonly$/
     * @throws ExpectationException Thrown by behat_base::find
     * @param string $element Element we look in
     * @param string $selectortype The type of element where we are looking in.
     */
    public function the_element_should_not_be_readonly($element, $selectortype) {
965
        $this->the_attribute_of_should_be_set("readonly", $element, $selectortype, true);
966
967
    }

968
    /**
969
970
971
     * Checks the provided element and selector type exists in the current page.
     *
     * This step is for advanced users, use it if you don't find anything else suitable for what you need.
972
     *
973
     * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should exist$/
974
975
976
977
     * @throws ElementNotFoundException Thrown by behat_base::find
     * @param string $element The locator of the specified selector
     * @param string $selectortype The selector type
     */
978
    public function should_exist($element, $selectortype) {
979
        // Will throw an ElementNotFoundException if it does not exist.
980
        $this->find($selectortype, $element);
981
982
983
    }

    /**
984
985
986
     * Checks that the provided element and selector type not exists in the current page.
     *
     * This step is for advanced users, use it if you don't find anything else suitable for what you need.
987
     *
988
     * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not exist$/
989
990
991
992
     * @throws ExpectationException
     * @param string $element The locator of the specified selector
     * @param string $selectortype The selector type
     */
993
    public function should_not_exist($element, $selectortype) {
994
995
        // Will throw an ElementNotFoundException if it does not exist, but, actually it should not exist, so we try &
        // catch it.
996
        try {
997
998
999
            // The exception does not really matter as we will catch it and will never "explode".
            $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $element);

1000
            // Using the spin method as we want a reduced timeout but there is no need for a 0.1 seconds interval
For faster browsing, not all history is shown. View entire blame