behat_navigation.php 45.5 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/>.

/**
 * Navigation 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
use Behat\Mink\Exception\ExpectationException as ExpectationException;
use Behat\Mink\Exception\DriverException as DriverException;
32
use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
33

34
35
36
37
38
39
40
41
42
43
/**
 * Steps definitions to navigate through the navigation tree nodes.
 *
 * @package    core
 * @category   test
 * @copyright  2012 David Monllaó
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class behat_navigation extends behat_base {

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    /**
     * Helper function to get a navigation nodes text element given its text from within the navigation block.
     *
     * This function finds the node with the given text from within the navigation block.
     * It checks to make sure the node is visible, and then returns it.
     *
     * @param string $text
     * @param bool $branch Set this true if you're only interested in the node if its a branch.
     * @param null|bool $collapsed Set this to true or false if you want the node to either be collapsed or not.
     *    If its left as null then we don't worry about it.
     * @param null|string|Exception|false $exception The exception to throw if the node is not found.
     * @return \Behat\Mink\Element\NodeElement
     */
    protected function get_node_text_node($text, $branch = false, $collapsed = null, $exception = null) {
        if ($exception === null) {
            $exception = new ExpectationException('The "' . $text . '" node could not be found', $this->getSession());
        } else if (is_string($exception)) {
            $exception = new ExpectationException($exception, $this->getSession());
        }

64
        $nodetextliteral = behat_context_helper::escape($text);
65
66
        $hasblocktree = "[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]";
        $hasbranch = "[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]";
67
68
        $hascollapsed = "li[@aria-expanded='false']/p";
        $notcollapsed = "li[@aria-expanded='true']/p";
69
70
71
72
73
74
75
76
77
        $match = "[normalize-space(.)={$nodetextliteral}]";

        // Avoid problems with quotes.
        $isbranch = ($branch) ? $hasbranch : '';
        if ($collapsed === true) {
            $iscollapsed = $hascollapsed;
        } else if ($collapsed === false) {
            $iscollapsed = $notcollapsed;
        } else {
78
            $iscollapsed = 'li/p';
79
80
        }

81
        // First check root nodes, it can be a span or link.
82
83
        $xpath  = "//ul{$hasblocktree}/{$hascollapsed}{$isbranch}/span{$match}|";
        $xpath  .= "//ul{$hasblocktree}/{$hascollapsed}{$isbranch}/a{$match}|";
84

85
        // Next search for the node containing the text within a link.
86
        $xpath .= "//ul{$hasblocktree}//ul/{$iscollapsed}{$isbranch}/a{$match}|";
87

88
        // Finally search for the node containing the text within a span.
89
        $xpath .= "//ul{$hasblocktree}//ul/{$iscollapsed}{$isbranch}/span{$match}";
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

        $node = $this->find('xpath', $xpath, $exception);
        $this->ensure_node_is_visible($node);
        return $node;
    }

    /**
     * Returns true if the navigation node with the given text is expandable.
     *
     * @Given /^navigation node "([^"]*)" should be expandable$/
     *
     * @throws ExpectationException
     * @param string $nodetext
     * @return bool
     */
    public function navigation_node_should_be_expandable($nodetext) {
        if (!$this->running_javascript()) {
            // Nodes are only expandable when JavaScript is enabled.
            return false;
        }

        $node = $this->get_node_text_node($nodetext, true);
        $node = $node->getParent();
113
114
        if ($node->hasClass('emptybranch')) {
            throw new ExpectationException('The "' . $nodetext . '" node is not expandable', $this->getSession());
115
        }
116
117

        return true;
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
    }

    /**
     * Returns true if the navigation node with the given text is not expandable.
     *
     * @Given /^navigation node "([^"]*)" should not be expandable$/
     *
     * @throws ExpectationException
     * @param string $nodetext
     * @return bool
     */
    public function navigation_node_should_not_be_expandable($nodetext) {
        if (!$this->running_javascript()) {
            // Nodes are only expandable when JavaScript is enabled.
            return false;
        }

        $node = $this->get_node_text_node($nodetext);
        $node = $node->getParent();
137
138
139

        if ($node->hasClass('emptybranch') || $node->hasClass('tree_item')) {
            return true;
140
        }
141
        throw new ExpectationException('The "' . $nodetext . '" node is expandable', $this->getSession());
142
143
    }

144
145
146
147
148
149
150
151
    /**
     * Click on an entry in the user menu.
     * @Given /^I follow "(?P<nodetext_string>(?:[^"]|\\")*)" in the user menu$/
     *
     * @param string $nodetext
     */
    public function i_follow_in_the_user_menu($nodetext) {

152
153
        if ($this->running_javascript()) {
            // The user menu must be expanded when JS is enabled.
154
            $xpath = "//div[contains(concat(' ', @class, ' '),  ' usermenu ')]//a[contains(concat(' ', @class, ' '), ' dropdown-toggle ')]";
155
            $this->execute("behat_general::i_click_on", array($this->escape($xpath), "xpath_element"));
156
157
        }

158
159
        // Now select the link.
        // The CSS path is always present, with or without JS.
160
        $csspath = ".usermenu .dropdown-menu";
161

162
        $this->execute('behat_general::i_click_on_in_the',
163
                array($nodetext, "link", $csspath, "css_element")
164
        );
165
166
    }

167
168
169
    /**
     * Expands the selected node of the navigation tree that matches the text.
     * @Given /^I expand "(?P<nodetext_string>(?:[^"]|\\")*)" node$/
170
     *
171
     * @throws ExpectationException
172
     * @param string $nodetext
173
     * @return bool|void
174
175
176
     */
    public function i_expand_node($nodetext) {

177
178
179
180
        // This step is useless with Javascript disabled as Moodle auto expands
        // all of tree's nodes; adding this because of scenarios that shares the
        // same steps with and without Javascript enabled.
        if (!$this->running_javascript()) {
181
182
            if ($nodetext === get_string('administrationsite')) {
                // Administration menu is not loaded by default any more. Click the link to expand.
183
184
185
186
                $this->execute('behat_general::i_click_on_in_the',
                    array($nodetext, "link", get_string('administration'), "block")
                );
                return true;
187
            }
188
            return true;
189
190
        }

191
192
193
194
195
196
        $node = $this->get_node_text_node($nodetext, true, true, 'The "' . $nodetext . '" node can not be expanded');
        // Check if the node is a link AND a branch.
        if (strtolower($node->getTagName()) === 'a') {
            // We just want to expand the node, we don't want to follow it.
            $node = $node->getParent();
        }
197
        $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
198
199
    }

200
201
202
203
204
205
    /**
     * Collapses the selected node of the navigation tree that matches the text.
     *
     * @Given /^I collapse "(?P<nodetext_string>(?:[^"]|\\")*)" node$/
     * @throws ExpectationException
     * @param string $nodetext
206
     * @return bool|void
207
208
209
210
211
     */
    public function i_collapse_node($nodetext) {

        // No collapsible nodes with non-JS browsers.
        if (!$this->running_javascript()) {
212
            return true;
213
214
        }

215
216
217
218
219
220
        $node = $this->get_node_text_node($nodetext, true, false, 'The "' . $nodetext . '" node can not be collapsed');
        // Check if the node is a link AND a branch.
        if (strtolower($node->getTagName()) === 'a') {
            // We just want to expand the node, we don't want to follow it.
            $node = $node->getParent();
        }
221
        $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
222
    }
223

224
    /**
225
     * Finds a node in the Navigation or Administration tree
226
227
228
     *
     * @param string $nodetext
     * @param array $parentnodes
229
230
231
     * @param string $nodetype node type (link or text)
     * @return NodeElement|null
     * @throws ExpectationException when one of the parent nodes is not found
232
     */
233
    protected function find_node_in_navigation($nodetext, $parentnodes, $nodetype = 'link') {
234
235
236
        // Site admin is different and needs special treatment.
        $siteadminstr = get_string('administrationsite');

237
238
239
        // Create array of all parentnodes.
        $countparentnode = count($parentnodes);

240
241
242
243
244
245
        // If JS is disabled and Site administration is not expanded we
        // should follow it, so all the lower-level nodes are available.
        if (!$this->running_javascript()) {
            if ($parentnodes[0] === $siteadminstr) {
                // We don't know if there if Site admin is already expanded so
                // don't wait, it is non-JS and we already waited for the DOM.
246
247
                $siteadminlink = $this->getSession()->getPage()->find('named_exact', array('link', "'" . $siteadminstr . "'"));
                if ($siteadminlink) {
248
                    $this->execute('behat_general::i_click_on', [$siteadminlink, 'NodeElement']);
249
250
251
252
                }
            }
        }

253
        // Get top level node.
254
255
        $node = $this->get_top_navigation_node($parentnodes[0]);

256
257
258
259
260
261
262
        // Expand all nodes.
        for ($i = 0; $i < $countparentnode; $i++) {
            if ($i > 0) {
                // Sub nodes within top level node.
                $node = $this->get_navigation_node($parentnodes[$i], $node);
            }

263
264
265
            // The p node contains the aria jazz.
            $pnodexpath = "/p[contains(concat(' ', normalize-space(@class), ' '), ' tree_item ')]";
            $pnode = $node->find('xpath', $pnodexpath);
266
            $linode = $pnode->getParent();
267

268
            // Keep expanding all sub-parents if js enabled.
269
270
            if ($pnode && $this->running_javascript() && $linode->hasAttribute('aria-expanded') &&
                ($linode->getAttribute('aria-expanded') == "false")) {
271
                $this->js_trigger_click($pnode);
272

273
                // Wait for node to load, if not loaded before.
274
275
                if ($linode->hasAttribute('data-loaded') && $linode->getAttribute('data-loaded') == "false") {
                    $jscondition = '(document.evaluate("' . $linode->getXpath() . '", document, null, '.
276
                        'XPathResult.ANY_TYPE, null).iterateNext().getAttribute(\'data-loaded\') == "true")';
277

278
                    $this->getSession()->wait(behat_base::get_extended_timeout() * 1000, $jscondition);
279
280
281
282
283
                }
            }
        }

        // Finally, click on requested node under navigation.
284
        $nodetextliteral = behat_context_helper::escape($nodetext);
285
        $tagname = ($nodetype === 'link') ? 'a' : 'span';
286
        $xpath = "/ul/li/p[contains(concat(' ', normalize-space(@class), ' '), ' tree_item ')]" .
287
288
289
            "/{$tagname}[normalize-space(.)=" . $nodetextliteral . "]";
        return $node->find('xpath', $xpath);
    }
290

291
292
293
294
295
296
297
298
299
    /**
     * Finds a node in the Navigation or Administration tree and clicks on it.
     *
     * @param string $nodetext
     * @param array $parentnodes
     * @throws ExpectationException
     */
    protected function select_node_in_navigation($nodetext, $parentnodes) {
        $nodetoclick = $this->find_node_in_navigation($nodetext, $parentnodes);
300
        // Throw exception if no node found.
301
        if (!$nodetoclick) {
302
            throw new ExpectationException('Navigation node "' . $nodetext . '" not found under "' .
303
                implode(' > ', $parentnodes) . '"', $this->getSession());
304
        }
305
        $this->execute('behat_general::i_click_on', [$nodetoclick, 'NodeElement']);
306
    }
307
308
309
310
311
312
313
314
315
316
317

    /**
     * Helper function to get top navigation node in tree.
     *
     * @throws ExpectationException if note not found.
     * @param string $nodetext name of top navigation node in tree.
     * @return NodeElement
     */
    protected function get_top_navigation_node($nodetext) {

        // Avoid problems with quotes.
318
        $nodetextliteral = behat_context_helper::escape($nodetext);
319
320
321
        $exception = new ExpectationException('Top navigation node "' . $nodetext . ' not found in "', $this->getSession());

        // First find in navigation block.
322
323
324
325
326
327
328
329
330
331
332
        $xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' card-text ')]" .
                "/ul[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]" .
                "/li[contains(concat(' ', normalize-space(@class), ' '), ' contains_branch ')]" .
                "/ul/li[contains(concat(' ', normalize-space(@class), ' '), ' contains_branch ')]" .
                "[p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" .
                "/*[contains(normalize-space(.), " . $nodetextliteral .")]]" .
                "|" .
                "//div[contains(concat(' ', normalize-space(@class), ' '), ' card-text ')]/div" .
                "/ul[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]" .
                "/li[p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" .
                "/*[contains(normalize-space(.), " . $nodetextliteral .")]]";
333
334

        $node = $this->find('xpath', $xpath, $exception);
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349

        return $node;
    }

    /**
     * Helper function to get sub-navigation node.
     *
     * @throws ExpectationException if note not found.
     * @param string $nodetext node to find.
     * @param NodeElement $parentnode parent navigation node.
     * @return NodeElement.
     */
    protected function get_navigation_node($nodetext, $parentnode = null) {

        // Avoid problems with quotes.
350
        $nodetextliteral = behat_context_helper::escape($nodetext);
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368

        $xpath = "/ul/li[contains(concat(' ', normalize-space(@class), ' '), ' contains_branch ')]" .
            "[child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" .
            "/child::span[normalize-space(.)=" . $nodetextliteral ."]]";
        $node = $parentnode->find('xpath', $xpath);
        if (!$node) {
            $xpath = "/ul/li[contains(concat(' ', normalize-space(@class), ' '), ' contains_branch ')]" .
                "[child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" .
                "/child::a[normalize-space(.)=" . $nodetextliteral ."]]";
            $node = $parentnode->find('xpath', $xpath);
        }

        if (!$node) {
            throw new ExpectationException('Sub-navigation node "' . $nodetext . '" not found under "' .
                $parentnode->getText() . '"', $this->getSession());
        }
        return $node;
    }
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393

    /**
     * Step to open the navigation bar if it is needed.
     *
     * The top log in and log out links are hidden when middle or small
     * size windows (or devices) are used. This step returns a step definition
     * clicking to expand the navbar if it is hidden.
     *
     * @Given /^I expand navigation bar$/
     */
    public function get_expand_navbar_step() {

        // Checking if we need to click the navbar button to show the navigation menu, it
        // is hidden by default when using clean theme and a medium or small screen size.

        // The DOM and the JS should be all ready and loaded. Running without spinning
        // as this is a widely used step and we can not spend time here trying to see
        // a DOM node that is not always there (at the moment clean is not even the
        // default theme...).
        $navbuttonjs = "return (
            Y.one('.btn-navbar') &&
            Y.one('.btn-navbar').getComputedStyle('display') !== 'none'
        )";

        // Adding an extra click we need to show the 'Log in' link.
394
        if (!$this->evaluate_script($navbuttonjs)) {
395
396
397
            return false;
        }

398
        $this->execute('behat_general::i_click_on', array(".btn-navbar", "css_element"));
399
    }
400
401
402
403
404
405
406
407
408
409
410
411
412

    /**
     * Go to current page setting item
     *
     * This can be used on front page, course, category or modules pages.
     *
     * @Given /^I navigate to "(?P<nodetext_string>(?:[^"]|\\")*)" in current page administration$/
     *
     * @throws ExpectationException
     * @param string $nodetext navigation node to click, may contain path, for example "Reports > Overview"
     * @return void
     */
    public function i_navigate_to_in_current_page_administration($nodetext) {
413
414
        $nodelist = array_map('trim', explode('>', $nodetext));
        $this->select_from_administration_menu($nodelist);
415
416
417
418
419
420
421
422
423
424
    }

    /**
     * Checks that current page administration contains text
     *
     * @Given /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should exist in current page administration$/
     *
     * @throws ExpectationException
     * @param string $element The locator of the specified selector.
     *     This may be a path, for example "Subscription mode > Forced subscription"
425
     * @param string $selectortype The selector type (link or text)
426
427
428
     * @return void
     */
    public function should_exist_in_current_page_administration($element, $selectortype) {
429
430
        $nodes = array_map('trim', explode('>', $element));
        $nodetext = end($nodes);
431

432
433
434
435
436
437
        // Find administration menu.
        $menuxpath = $this->find_header_administration_menu() ?: $this->find_page_administration_menu(true);

        $this->toggle_page_administration_menu($menuxpath);
        $this->execute('behat_general::should_exist_in_the', [$nodetext, $selectortype, $menuxpath, 'xpath_element']);
        $this->toggle_page_administration_menu($menuxpath);
438
439
440
441
442
443
444
445
446
447
    }

    /**
     * Checks that current page administration contains text
     *
     * @Given /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not exist in current page administration$/
     *
     * @throws ExpectationException
     * @param string $element The locator of the specified selector.
     *     This may be a path, for example "Subscription mode > Forced subscription"
448
     * @param string $selectortype The selector type (link or text)
449
450
451
     * @return void
     */
    public function should_not_exist_in_current_page_administration($element, $selectortype) {
452
453
454
455
456
457
458
459
        $nodes = array_map('trim', explode('>', $element));
        $nodetext = end($nodes);

        // Find administration menu.
        $menuxpath = $this->find_header_administration_menu() ?: $this->find_page_administration_menu();
        if (!$menuxpath) {
            // Menu not found, exit.
            return;
460
        }
461
462
463
464

        $this->toggle_page_administration_menu($menuxpath);
        $this->execute('behat_general::should_not_exist_in_the', [$nodetext, $selectortype, $menuxpath, 'xpath_element']);
        $this->toggle_page_administration_menu($menuxpath);
465
466
467
468
469
470
471
472
473
474
475
476
    }

    /**
     * Go to site administration item
     *
     * @Given /^I navigate to "(?P<nodetext_string>(?:[^"]|\\")*)" in site administration$/
     *
     * @throws ExpectationException
     * @param string $nodetext navigation node to click, may contain path, for example "Reports > Overview"
     * @return void
     */
    public function i_navigate_to_in_site_administration($nodetext) {
477
478
479
        $nodelist = array_map('trim', explode('>', $nodetext));
        $this->i_select_from_flat_navigation_drawer(get_string('administrationsite'));
        $this->select_on_administration_page($nodelist);
480
    }
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500

    /**
     * Opens the current users profile page in edit mode.
     *
     * @Given /^I open my profile in edit mode$/
     * @throws coding_exception
     * @return void
     */
    public function i_open_my_profile_in_edit_mode() {
        global $USER;

        $user = $this->get_session_user();
        $globuser = $USER;
        $USER = $user; // We need this set to the behat session user so we can call isloggedin.

        $systemcontext = context_system::instance();

        $bodynode = $this->find('xpath', 'body');
        $bodyclass = $bodynode->getAttribute('class');
        $matches = [];
501
        if (preg_match('/(?<=^course-|\scourse-)\d+/', $bodyclass, $matches) && !empty($matches)) {
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
            $courseid = intval($matches[0]);
        } else {
            $courseid = SITEID;
        }

        if (isloggedin() && !isguestuser($user) && !is_mnet_remote_user($user)) {
            if (is_siteadmin($user) ||  has_capability('moodle/user:update', $systemcontext)) {
                $url = new moodle_url('/user/editadvanced.php', array('id' => $user->id, 'course' => SITEID,
                    'returnto' => 'profile'));
            } else if (has_capability('moodle/user:editownprofile', $systemcontext)) {
                $userauthplugin = false;
                if (!empty($user->auth)) {
                    $userauthplugin = get_auth_plugin($user->auth);
                }
                if ($userauthplugin && $userauthplugin->can_edit_profile()) {
                    $url = $userauthplugin->edit_profile_url();
                    if (empty($url)) {
                        if (empty($course)) {
                            $url = new moodle_url('/user/edit.php', array('id' => $user->id, 'returnto' => 'profile'));
                        } else {
                            $url = new moodle_url('/user/edit.php', array('id' => $user->id, 'course' => $courseid,
                                'returnto' => 'profile'));
                        }
                    }

                }
            }
529
            $this->execute('behat_general::i_visit', [$url]);
530
531
532
533
534
535
        }

        // Restore global user variable.
        $USER = $globuser;
    }

536
537
538
539
540
541
542
543
544
545
    /**
     * Open a given page, belonging to a plugin or core component.
     *
     * The page-type are interpreted by each plugin to work out the
     * corresponding URL. See the resolve_url method in each class like
     * behat_mod_forum. That method should document which page types are
     * recognised, and how the name identifies them.
     *
     * For pages belonging to core, the 'core > ' bit is omitted.
     *
546
547
548
     * @When /^I am on the (?<page>[^ "]*) page$/
     * @When /^I am on the "(?<page>[^"]*)" page$/
     *
549
550
551
552
553
     * @param string $page the component and page name.
     *      E.g. 'Admin notifications' or 'core_user > Preferences'.
     * @throws Exception if the specified page cannot be determined.
     */
    public function i_am_on_page(string $page) {
554
        $this->execute('behat_general::i_visit', [$this->resolve_page_helper($page)]);
555
556
557
558
559
560
561
562
563
564
565
    }

    /**
     * Open a given page logged in as a given user.
     *
     * This is like the combination
     *   When I log in as "..."
     *   And I am on the "..." page
     * but with the advantage that you go straight to the desired page, without
     * having to wait for the Dashboard to load.
     *
566
567
568
569
570
     * @When /^I am on the (?<page>[^ "]*) page logged in as (?<username>[^ "]*)$/
     * @When /^I am on the "(?<page>[^"]*)" page logged in as (?<username>[^ "]*)$/
     * @When /^I am on the (?<page>[^ "]*) page logged in as "(?<username>[^ "]*)"$/
     * @When /^I am on the "(?<page>[^"]*)" page logged in as "(?<username>[^ "]*)"$/
     *
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
     * @param string $page the type of page. E.g. 'Admin notifications' or 'core_user > Preferences'.
     * @param string $username the name of the user to log in as. E.g. 'admin'.
     * @throws Exception if the specified page cannot be determined.
     */
    public function i_am_on_page_logged_in_as(string $page, string $username) {
        self::execute('behat_auth::i_log_in_as', [$username, $this->resolve_page_helper($page)]);
    }

    /**
     * Helper used by i_am_on_page() and i_am_on_page_logged_in_as().
     *
     * @param string $page the type of page. E.g. 'Admin notifications' or 'core_user > Preferences'.
     * @return moodle_url the corresponding URL.
     */
    protected function resolve_page_helper(string $page): moodle_url {
586
587
        list($component, $name) = $this->parse_page_name($page);
        if ($component === 'core') {
588
            return $this->resolve_core_page_url($name);
589
590
        } else {
            $context = behat_context_helper::get('behat_' . $component);
591
            return $context->resolve_page_url($name);
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
        }
    }

    /**
     * Parse a full page name like 'Admin notifications' or 'core_user > Preferences'.
     *
     * E.g. parsing 'mod_quiz > View' gives ['mod_quiz', 'View'].
     *
     * @param string $page the full page name
     * @return array with two elements, component and page name.
     */
    protected function parse_page_name(string $page): array {
        $dividercount = substr_count($page, ' > ');
        if ($dividercount === 0) {
            return ['core', $page];
        } else if ($dividercount === 1) {
            list($component, $name) = explode(' > ', $page);
            if ($component === 'core') {
                throw new coding_exception('Do not specify the component "core > ..." for core pages.');
            }
            return [$component, $name];
        } else {
            throw new coding_exception('The page name most be in the form ' .
                    '"{page-name}" for core pages, or "{component} > {page-name}" ' .
                    'for pages belonging to other components. ' .
                    'For example "Admin notifications" or "mod_quiz > View".');
        }
    }

    /**
     * Open a given instance of a page, belonging to a plugin or core component.
     *
     * The instance identifier and page-type are interpreted by each plugin to
     * work out the corresponding URL. See the resolve_page_instance_url method
     * in each class like behat_mod_forum. That method should document which page
     * types are recognised, and how the name identifies them.
     *
     * For pages belonging to core, the 'core > ' bit is omitted.
     *
631
632
633
634
635
     * @When /^I am on the (?<identifier>[^ "]*) (?<type>[^ "]*) page$/
     * @When /^I am on the "(?<identifier>[^"]*)" "(?<type>[^"]*)" page$/
     * @When /^I am on the (?<identifier>[^ "]*) "(?<type>[^"]*)" page$/
     * @When /^I am on the "(?<identifier>[^"]*)" (?<type>[^ "]*) page$/
     *
636
637
638
639
640
     * @param string $identifier identifies the particular page. E.g. 'Test quiz'.
     * @param string $type the component and page type. E.g. 'mod_quiz > View'.
     * @throws Exception if the specified page cannot be determined.
     */
    public function i_am_on_page_instance(string $identifier, string $type) {
641
        $this->execute('behat_general::i_visit', [$this->resolve_page_instance_helper($identifier, $type)]);
642
643
644
645
646
647
648
649
650
651
652
    }

    /**
     * Open a given page logged in as a given user.
     *
     * This is like the combination
     *   When I log in as "..."
     *   And I am on the "..." "..." page
     * but with the advantage that you go straight to the desired page, without
     * having to wait for the Dashboard to load.
     *
653
654
655
656
657
658
659
660
661
     * @When /^I am on the (?<identifier>[^ "]*) (?<type>[^ "]*) page logged in as (?<username>[^ "]*)$/
     * @When /^I am on the "(?<identifier>[^"]*)" "(?<type>[^"]*)" page logged in as (?<username>[^ "]*)$/
     * @When /^I am on the (?<identifier>[^ "]*) "(?<type>[^"]*)" page logged in as (?<username>[^ "]*)$/
     * @When /^I am on the "(?<identifier>[^"]*)" (?<type>[^ "]*) page logged in as (?<username>[^ "]*)$/
     * @When /^I am on the (?<identifier>[^ "]*) (?<type>[^ "]*) page logged in as "(?<username>[^"]*)"$/
     * @When /^I am on the "(?<identifier>[^"]*)" "(?<type>[^"]*)" page logged in as "(?<username>[^"]*)"$/
     * @When /^I am on the (?<identifier>[^ "]*) "(?<type>[^"]*)" page logged in as "(?<username>[^"]*)"$/
     * @When /^I am on the "(?<identifier>[^"]*)" (?<type>[^ "]*) page logged in as "(?<username>[^"]*)"$/
     *
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
     * @param string $identifier identifies the particular page. E.g. 'Test quiz'.
     * @param string $type the component and page type. E.g. 'mod_quiz > View'.
     * @param string $username the name of the user to log in as. E.g. 'student'.
     * @throws Exception if the specified page cannot be determined.
     */
    public function i_am_on_page_instance_logged_in_as(string $identifier,
            string $type, string $username) {
        self::execute('behat_auth::i_log_in_as',
                [$username, $this->resolve_page_instance_helper($identifier, $type)]);
    }

    /**
     * Helper used by i_am_on_page() and i_am_on_page_logged_in_as().
     *
     * @param string $identifier identifies the particular page. E.g. 'Test quiz'.
     * @param string $pagetype the component and page type. E.g. 'mod_quiz > View'.
     * @return moodle_url the corresponding URL.
     */
    protected function resolve_page_instance_helper(string $identifier, string $pagetype): moodle_url {
        list($component, $type) = $this->parse_page_name($pagetype);
682
        if ($component === 'core') {
683
            return $this->resolve_core_page_instance_url($type, $identifier);
684
685
        } else {
            $context = behat_context_helper::get('behat_' . $component);
686
            return $context->resolve_page_instance_url($type, $identifier);
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
        }
    }

    /**
     * Convert core page names to URLs for steps like 'When I am on the "[page name]" page'.
     *
     * Recognised page names are:
     * | Homepage            | Homepage (normally dashboard).                                 |
     * | Admin notifications | Admin notification screen.                                     |
     *
     * @param string $name identifies which identifies this page, e.g. 'Homepage', 'Admin notifications'.
     * @return moodle_url the corresponding URL.
     * @throws Exception with a meaningful error message if the specified page cannot be found.
     */
    protected function resolve_core_page_url(string $name): moodle_url {
        switch ($name) {
            case 'Homepage':
                return new moodle_url('/');

            case 'Admin notifications':
                return new moodle_url('/admin/');

            default:
                throw new Exception('Unrecognised core page type "' . $name . '."');
        }
    }

    /**
     * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
     *
     * Recognised page names are:
718
719
720
721
722
723
724
725
726
727
728
     * | Page type                  | Identifier meaning        | description                          |
     * | Category                   | category idnumber         | List of courses in that category.    |
     * | Course                     | course shortname          | Main course home pag                 |
     * | Activity                   | activity idnumber         | Start page for that activity         |
     * | Activity editing           | activity idnumber         | Edit settings page for that activity |
     * | [modname] Activity         | activity name or idnumber | Start page for that activity         |
     * | [modname] Activity editing | activity name or idnumber | Edit settings page for that activity |
     *
     * Examples:
     *
     * When I am on the "Welcome to ECON101" "forum activity" page logged in as student1
729
730
731
732
733
734
735
736
737
     *
     * @param string $type identifies which type of page this is, e.g. 'Category page'.
     * @param string $identifier identifies the particular page, e.g. 'test-cat'.
     * @return moodle_url the corresponding URL.
     * @throws Exception with a meaningful error message if the specified page cannot be found.
     */
    protected function resolve_core_page_instance_url(string $type, string $identifier): moodle_url {
        global $DB;

738
739
740
        $type = strtolower($type);

        switch ($type) {
741
            case 'category':
742
                $categoryid = $this->get_category_id($identifier);
743
744
745
                if (!$categoryid) {
                    throw new Exception('The specified category with idnumber "' . $identifier . '" does not exist');
                }
746
                return new moodle_url('/course/index.php', ['categoryid' => $categoryid]);
747

748
749
            case 'course':
                $courseid = $this->get_course_id($identifier);
750
751
752
753
754
755
                if (!$courseid) {
                    throw new Exception('The specified course with shortname, fullname, or idnumber "' .
                            $identifier . '" does not exist');
                }
                return new moodle_url('/course/view.php', ['id' => $courseid]);

756
757
            case 'activity':
                $cm = $this->get_course_module_for_identifier($identifier);
758
759
760
                if (!$cm) {
                    throw new Exception('The specified activity with idnumber "' . $identifier . '" does not exist');
                }
761
                return $cm->url;
762

763
764
765
766
767
768
769
770
            case 'activity editing':
                $cm = $this->get_course_module_for_identifier($identifier);
                if (!$cm) {
                    throw new Exception('The specified activity with idnumber "' . $identifier . '" does not exist');
                }
                return new moodle_url('/course/modedit.php', [
                    'update' => $cm->id,
                ]);
771
        }
772

773
774
775
776
777
778
779
        $parts = explode(' ', $type);
        if (count($parts) > 1) {
            if ($parts[1] === 'activity') {
                $modname = $parts[0];
                $cm = $this->get_cm_by_activity_name($modname, $identifier);

                if (count($parts) == 2) {
780
                    // View page.
781
782
783
784
                    return new moodle_url($cm->url);
                }

                if ($parts[2] === 'editing') {
785
                    // Edit settings page.
786
787
                    return new moodle_url('/course/modedit.php', ['update' => $cm->id]);
                }
788
789
790
791
792
793
794
795
796
797

                if ($parts[2] === 'roles') {
                    // Locally assigned roles page.
                    return new moodle_url('/admin/roles/assign.php', ['contextid' => $cm->context->id]);
                }

                if ($parts[2] === 'permissions') {
                    // Permissions page.
                    return new moodle_url('/admin/roles/permissions.php', ['contextid' => $cm->context->id]);
                }
798
            }
799
        }
800
801

        throw new Exception('Unrecognised core page type "' . $type . '."');
802
803
    }

804
    /**
805
     * Opens the course homepage. (Consider using 'I am on the "shortname" "Course" page' step instead.)
806
     *
807
     * @Given /^I am on "(?P<coursefullname_string>(?:[^"]|\\")*)" course homepage$/
808
     * @throws coding_exception
jun's avatar
jun committed
809
     * @param string $coursefullname The full name of the course.
810
811
     * @return void
     */
812
    public function i_am_on_course_homepage($coursefullname) {
813
814
        global $DB;
        $course = $DB->get_record("course", array("fullname" => $coursefullname), 'id', MUST_EXIST);
815
        $url = new moodle_url('/course/view.php', ['id' => $course->id]);
816
        $this->execute('behat_general::i_visit', [$url]);
817
    }
818

819
    /**
820
     * Open the course homepage with editing mode enabled.
821
822
823
     *
     * @Given /^I am on "(?P<coursefullname_string>(?:[^"]|\\")*)" course homepage with editing mode on$/
     * @throws coding_exception
jun's avatar
jun committed
824
     * @param string $coursefullname The course full name of the course.
825
826
827
828
     * @return void
     */
    public function i_am_on_course_homepage_with_editing_mode_on($coursefullname) {
        global $DB;
829

830
        $course = $DB->get_record("course", array("fullname" => $coursefullname), 'id', MUST_EXIST);
831
        $url = new moodle_url('/course/view.php', ['id' => $course->id]);
832
833
834
835
836

        if ($this->running_javascript() && $sesskey = $this->get_sesskey()) {
            // Javascript is running so it is possible to grab the session ket and jump straight to editing mode.
            $url->param('edit', 1);
            $url->param('sesskey', $sesskey);
837
            $this->execute('behat_general::i_visit', [$url]);
838
839
840
841
842

            return;
        }

        // Visit the course page.
843
        $this->execute('behat_general::i_visit', [$url]);
844

845
846
847
848
849
        try {
            $this->execute("behat_forms::press_button", get_string('turneditingon'));
        } catch (Exception $e) {
            $this->execute("behat_navigation::i_navigate_to_in_current_page_administration", [get_string('turneditingon')]);
        }
850
    }
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866

    /**
     * Opens the flat navigation drawer if it is not already open
     *
     * @When /^I open flat navigation drawer$/
     * @throws ElementNotFoundException Thrown by behat_base::find
     */
    public function i_open_flat_navigation_drawer() {
        if (!$this->running_javascript()) {
            // Navigation drawer is always open without JS.
            return;
        }
        $xpath = "//button[contains(@data-action,'toggle-drawer')]";
        $node = $this->find('xpath', $xpath);
        $expanded = $node->getAttribute('aria-expanded');
        if ($expanded === 'false') {
867
            $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
            $this->ensure_node_attribute_is_set($node, 'aria-expanded', 'true');
        }
    }

    /**
     * Closes the flat navigation drawer if it is open (does nothing if JS disabled)
     *
     * @When /^I close flat navigation drawer$/
     * @throws ElementNotFoundException Thrown by behat_base::find
     */
    public function i_close_flat_navigation_drawer() {
        if (!$this->running_javascript()) {
            // Navigation drawer can not be closed without JS.
            return;
        }
        $xpath = "//button[contains(@data-action,'toggle-drawer')]";
        $node = $this->find('xpath', $xpath);
        $expanded = $node->getAttribute('aria-expanded');
        if ($expanded === 'true') {
887
            $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
        }
    }

    /**
     * Clicks link with specified id|title|alt|text in the flat navigation drawer.
     *
     * @When /^I select "(?P<link_string>(?:[^"]|\\")*)" from flat navigation drawer$/
     * @throws ElementNotFoundException Thrown by behat_base::find
     * @param string $link
     */
    public function i_select_from_flat_navigation_drawer($link) {
        $this->i_open_flat_navigation_drawer();
        $this->execute('behat_general::i_click_on_in_the', [$link, 'link', '#nav-drawer', 'css_element']);
    }

    /**
     * If we are not on the course main page, click on the course link in the navbar
     */
    protected function go_to_main_course_page() {
        $url = $this->getSession()->getCurrentUrl();
        if (!preg_match('|/course/view.php\?id=[\d]+$|', $url)) {
909
910
            $node = $this->find('xpath', '//header//div[@id=\'page-navbar\']//a[contains(@href,\'/course/view.php?id=\')]');
            $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
        }
    }

    /**
     * Finds and clicks a link on the admin page (site administration or course administration)
     *
     * @param array $nodelist
     */
    protected function select_on_administration_page($nodelist) {
        $parentnodes = $nodelist;
        $lastnode = array_pop($parentnodes);
        $xpath = '//section[@id=\'region-main\']';

        // Check if there is a separate tab for this submenu of the page. If found go to it.
        if ($parentnodes) {
            $tabname = behat_context_helper::escape($parentnodes[0]);
            $tabxpath = '//ul[@role=\'tablist\']/li/a[contains(normalize-space(.), ' . $tabname . ')]';
            if ($node = $this->getSession()->getPage()->find('xpath', $tabxpath)) {
                if ($this->running_javascript()) {
930
                    $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
                    // Click on the tab and add 'active' tab to the xpath.
                    $xpath .= '//div[contains(@class,\'active\')]';
                } else {
                    // Add the tab content selector to the xpath.
                    $tabid = behat_context_helper::escape(ltrim($node->getAttribute('href'), '#'));
                    $xpath .= '//div[@id = ' . $tabid . ']';
                }
                array_shift($parentnodes);
            }
        }

        // Find a section with the parent name in it.
        if ($parentnodes) {
            // Find the section on the page (links may be repeating in different sections).
            $section = behat_context_helper::escape($parentnodes[0]);
            $xpath .= '//div[@class=\'row\' and contains(.,'.$section.')]';
        }

        // Find a link and click on it.
        $linkname = behat_context_helper::escape($lastnode);
        $xpath .= '//a[contains(normalize-space(.), ' . $linkname . ')]';
        if (!$node = $this->getSession()->getPage()->find('xpath', $xpath)) {
953
            throw new ElementNotFoundException($this->getSession(), 'Link "' . join(' > ', $nodelist) . '"');
954
        }
955
        $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
956
957
958
959
960
961
962
963
964
965
966
    }

    /**
     * Locates the administration menu in the <header> element and returns its xpath
     *
     * @param bool $mustexist if specified throws an exception if menu is not found
     * @return null|string
     */
    protected function find_header_administration_menu($mustexist = false) {
        $menuxpath = '//header[@id=\'page-header\']//div[contains(@class,\'moodle-actionmenu\')]';
        if ($mustexist) {
967
            $exception = new ElementNotFoundException($this->getSession(), 'Page header administration menu');
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
            $this->find('xpath', $menuxpath, $exception);
        } else if (!$this->getSession()->getPage()->find('xpath', $menuxpath)) {
            return null;
        }
        return $menuxpath;
    }

    /**
     * Locates the administration menu on the page (but not in the header) and returns its xpath
     *
     * @param bool $mustexist if specified throws an exception if menu is not found
     * @return null|string
     */
    protected function find_page_administration_menu($mustexist = false) {
        $menuxpath = '//div[@id=\'region-main-settings-menu\']';
        if ($mustexist) {
984
            $exception = new ElementNotFoundException($this->getSession(), 'Page administration menu');
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
            $this->find('xpath', $menuxpath, $exception);
        } else if (!$this->getSession()->getPage()->find('xpath', $menuxpath)) {
            return null;
        }
        return $menuxpath;
    }

    /**
     * Toggles administration menu
     *
     * @param string $menuxpath (optional) xpath to the page administration menu if already known
     */
    protected function toggle_page_administration_menu($menuxpath = null) {
        if (!$menuxpath) {
            $menuxpath = $this->find_header_administration_menu() ?: $this->find_page_administration_menu();
        }
For faster browsing, not all history is shown. View entire blame