Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Behat course-related steps definitions.
19
 *
20
 * @package    core_course
21
 * @category   test
22
 * @copyright  2012 David Monllaó
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
27
 
28
require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
29
 
30
use Behat\Gherkin\Node\TableNode as TableNode,
31
    Behat\Mink\Exception\ExpectationException as ExpectationException,
32
    Behat\Mink\Exception\DriverException as DriverException,
33
    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
34
 
35
/**
36
 * Course-related steps definitions.
37
 *
38
 * @package    core_course
39
 * @category   test
40
 * @copyright  2012 David Monllaó
41
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42
 */
43
class behat_course extends behat_base {
44
 
45
    /**
46
     * Return the list of partial named selectors.
47
     *
48
     * @return array
49
     */
50
    public static function get_partial_named_selectors(): array {
51
        return [
52
            new behat_component_named_selector(
53
                'Activity chooser screen', [
54
                    "%core_course/activityChooser%//*[@data-region=%locator%][contains(concat(' ', @class, ' '), ' carousel-item ')]"
55
                ]
56
            ),
57
            new behat_component_named_selector(
58
                'Activity chooser tab', [
59
                    "%core_course/activityChooser%//*[@data-region=%locator%][contains(concat(' ', @class, ' '), ' tab-pane ')]"
60
                ]
61
            ),
62
        ];
63
    }
64
 
65
    /**
66
     * Return a list of the Mink named replacements for the component.
67
     *
68
     * Named replacements allow you to define parts of an xpath that can be reused multiple times, or in multiple
69
     * xpaths.
70
     *
71
     * This method should return a list of {@link behat_component_named_replacement} and the docs on that class explain
72
     * how it works.
73
     *
74
     * @return behat_component_named_replacement[]
75
     */
76
    public static function get_named_replacements(): array {
77
        return [
78
            new behat_component_named_replacement(
79
                'activityChooser',
80
                ".//*[contains(concat(' ', @class, ' '), ' modchooser ')][contains(concat(' ', @class, ' '), ' modal-dialog ')]"
81
            ),
82
        ];
83
    }
84
 
85
    /**
86
     * Creates a new course with the provided table data matching course settings names with the desired values.
87
     *
88
     * @Given /^I create a course with:$/
89
     * @param TableNode $table The course data
90
     */
91
    public function i_create_a_course_with(TableNode $table) {
92
 
93
        // Go to course management page.
94
        $this->i_go_to_the_courses_management_page();
95
        // Ensure you are on course management page.
96
        $this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categoriesandcourses'));
97
 
98
        // Select default course category.
99
        $this->i_click_on_category_in_the_management_interface(get_string('defaultcategoryname'));
100
        $this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categoriesandcourses'));
101
 
102
        // Click create new course.
103
        $this->execute('behat_general::i_click_on_in_the',
104
            array(get_string('createnewcourse'), "link", "#course-listing", "css_element")
105
        );
106
 
107
        // If the course format is one of the fields we change how we
108
        // fill the form as we need to wait for the form to be set.
109
        $rowshash = $table->getRowsHash();
110
        $formatfieldrefs = array(get_string('format'), 'format', 'id_format');
111
        foreach ($formatfieldrefs as $fieldref) {
112
            if (!empty($rowshash[$fieldref])) {
113
                $formatfield = $fieldref;
114
            }
115
        }
116
 
117
        // Setting the format separately.
118
        if (!empty($formatfield)) {
119
 
120
            // Removing the format field from the TableNode.
121
            $rows = $table->getRows();
122
            $formatvalue = $rowshash[$formatfield];
123
            foreach ($rows as $key => $row) {
124
                if ($row[0] == $formatfield) {
125
                    unset($rows[$key]);
126
                }
127
            }
128
            $table = new TableNode($rows);
129
 
130
            // Adding a forced wait until editors are loaded as otherwise selenium sometimes tries clicks on the
131
            // format field when the editor is being rendered and the click misses the field coordinates.
132
            $this->execute("behat_forms::i_expand_all_fieldsets");
133
 
134
            $this->execute("behat_forms::i_set_the_field_to", array($formatfield, $formatvalue));
135
        }
136
 
137
        // Set form fields.
138
        $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $table);
139
 
140
        // Save course settings.
141
        $this->execute("behat_forms::press_button", get_string('savechangesanddisplay'));
142
 
143
    }
144
 
145
    /**
146
     * Goes to the system courses/categories management page.
147
     *
148
     * @Given /^I go to the courses management page$/
149
     */
150
    public function i_go_to_the_courses_management_page() {
151
 
152
        $parentnodes = get_string('courses', 'admin');
153
 
154
        // Go to home page.
155
        $this->execute("behat_general::i_am_on_homepage");
156
 
157
        // Navigate to course management via system administration.
158
        $this->execute("behat_navigation::i_navigate_to_in_site_administration",
159
            array($parentnodes . ' > ' . get_string('coursemgmt', 'admin'))
160
        );
161
 
162
    }
163
 
164
    /**
165
     *  Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
166
     *
167
     *  Recognised page names are:
168
     *  | Section | coursename > section | The selected course section. First it searchs by section name, then by section number. |
169
     *
170
     * Examples:
171
     *
172
     *  When I am on the "Course 1 > Section 1" "course > section" page logged in as "admin"
173
     *  When I am on the "Course 1 > Named section" "course > section" page logged in as "admin"
174
     *
175
     * @param string $type
176
     * @param string $identifier
177
     * @return moodle_url
178
     */
179
    protected function resolve_page_instance_url(string $type, string $identifier): moodle_url {
180
        $type = strtolower($type);
181
        switch ($type) {
182
            case 'section':
183
                $identifiers = explode('>', $identifier);
184
                $identifiers = array_map('trim', $identifiers);
185
                if (count($identifiers) < 2) {
186
                    throw new Exception("The specified section $identifier is not valid and should be coursename > section.");
187
                }
188
                [$courseidentifier, $sectionidentifier] = $identifiers;
189
 
190
                $section = $this->get_section_and_course_by_id($courseidentifier, $sectionidentifier);
191
                if (!$section) {
192
                    // If section is not found by name, search it by section number.
193
                    $sectionno = preg_replace("/^section (\d+)$/i", '$1', $sectionidentifier);
194
                    $section = $this->get_section_and_course_by_sectionnum($courseidentifier, (int) $sectionno);
195
                }
196
                if (!$section) {
197
                    throw new Exception("The specified section $identifier does not exist.");
198
                }
199
                return new moodle_url('/course/section.php', ['id' => $section->id]);
200
        }
201
        throw new Exception('Unrecognised core page type "' . $type . '."');
202
    }
203
 
204
    /**
205
     * Adds the selected activity/resource filling the form data with the specified field/value pairs.
206
     *
207
     * Sections 0 and 1 are also allowed on frontpage.
208
     *
209
     * @Given I add a :activity activity to course :coursefullname section :sectionnum and I fill the form with:
210
     * @Given I add an :activity activity to course :coursefullname section :sectionnum and I fill the form with:
211
     * @param string $activity The activity name
212
     * @param string $coursefullname The course full name of the course.
213
     * @param int $section The section number
214
     * @param TableNode $data The activity field/value data
215
     */
216
    public function i_add_to_course_section_and_i_fill_the_form_with($activity, $coursefullname, $section, TableNode $data) {
217
 
218
        // Add activity to section.
219
        $this->execute(
220
            "behat_course::i_add_to_course_section",
221
            [$this->escape($activity), $this->escape($coursefullname), $this->escape($section)]
222
        );
223
 
224
        // Wait to be redirected.
225
        $this->execute('behat_general::wait_until_the_page_is_ready');
226
 
227
        // Set form fields.
228
        $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
229
 
230
        // Save course settings.
231
        $this->execute("behat_forms::press_button", get_string('savechangesandreturntocourse'));
232
    }
233
 
234
    /**
235
     * Open a add activity form page.
236
     *
237
     * @Given I add a :activity activity to course :coursefullname section :sectionnum
238
     * @Given I add an :activity activity to course :coursefullname section :sectionnum
239
     * @throws coding_exception
240
     * @param string $activity The activity name.
241
     * @param string $coursefullname The course full name of the course.
242
     * @param string $sectionnum The section number.
243
     */
244
    public function i_add_to_course_section(string $activity, string $coursefullname, string $sectionnum): void {
245
        $addurl = new moodle_url('/course/modedit.php', [
246
            'add' => $activity,
247
            'course' => $this->get_course_id($coursefullname),
248
            'section' => intval($sectionnum),
249
        ]);
250
        $this->execute('behat_general::i_visit', [$addurl]);
251
    }
252
 
253
    /**
254
     * Opens the activity chooser and opens the activity/resource link form page. Sections 0 and 1 are also allowed on frontpage.
255
     *
256
     * This step require javascript enabled and it is used mainly to click activities or resources by name,
257
     * not by plugin name. Use the standard behat_course::i_add_to_course_section step instead unless the
258
     * plugin create extra entries into the activity chooser (like LTI).
259
     *
260
     * @Given I add a :activityname to section :sectionnum using the activity chooser
261
     * @Given I add an :activityname to section :sectionnum using the activity chooser
262
     * @throws ElementNotFoundException Thrown by behat_base::find
263
     * @param string $activityname
264
     * @param int $sectionnum
265
     */
266
    public function i_add_to_section_using_the_activity_chooser($activityname, $sectionnum) {
267
 
268
        $this->require_javascript('Please use the \'the following "activity" exists:\' data generator instead.');
269
 
270
        if ($this->getSession()->getPage()->find('css', 'body#page-site-index') && (int) $sectionnum <= 1) {
271
            // We are on the frontpage.
272
            if ($sectionnum) {
273
                // Section 1 represents the contents on the frontpage.
274
                $sectionxpath = "//body[@id='page-site-index']" .
275
                    "/descendant::div[contains(concat(' ',normalize-space(@class),' '),' sitetopic ')]";
276
            } else {
277
                // Section 0 represents "Site main menu" block.
278
                $sectionxpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]";
279
            }
280
        } else {
281
            // We are inside the course.
282
            $sectionxpath = "//li[@id='section-" . $sectionnum . "']";
283
        }
284
 
285
        // Clicks add activity or resource section link.
286
        $sectionnode = $this->find('xpath', $sectionxpath);
287
        $this->execute('behat_general::i_click_on_in_the', [
288
            "//button[@data-action='open-chooser' and not(@data-beforemod)]",
289
            'xpath',
290
            $sectionnode,
291
            'NodeElement',
292
        ]);
293
 
294
        // Clicks the selected activity if it exists.
295
        $activityliteral = behat_context_helper::escape(ucfirst($activityname));
296
        $activityxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' modchooser ')]" .
297
            "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optioninfo ')]" .
298
            "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optionname ')]" .
299
            "[normalize-space(.)=$activityliteral]" .
300
            "/parent::a";
301
 
302
        $this->execute('behat_general::i_click_on', [$activityxpath, 'xpath']);
303
    }
304
 
305
    /**
306
     * Opens a section edit menu if it is not already opened.
307
     *
308
     * @Given /^I open section "(?P<section_number>\d+)" edit menu$/
309
     * @throws DriverException The step is not available when Javascript is disabled
310
     * @param string $sectionnumber
311
     */
312
    public function i_open_section_edit_menu($sectionnumber) {
313
        if (!$this->running_javascript()) {
314
            throw new DriverException('Section edit menu not available when Javascript is disabled');
315
        }
316
 
317
        // Wait for section to be available, before clicking on the menu.
318
        $this->i_wait_until_section_is_available($sectionnumber);
319
 
320
        // If it is already opened we do nothing.
321
        $xpath = $this->section_exists($sectionnumber);
322
        $xpath .= "/descendant::div[contains(@class, 'section-actions')]/descendant::a[contains(@data-toggle, 'dropdown')]";
323
 
324
        $exception = new ExpectationException('Section "' . $sectionnumber . '" was not found', $this->getSession());
325
        $menu = $this->find('xpath', $xpath, $exception);
326
        $menu->click();
327
        $this->i_wait_until_section_is_available($sectionnumber);
328
    }
329
 
330
    /**
331
     * Deletes course section.
332
     *
333
     * @Given /^I delete section "(?P<section_number>\d+)"$/
334
     * @param int $sectionnumber The section number
335
     */
336
    public function i_delete_section($sectionnumber) {
337
        // Ensures the section exists.
338
        $xpath = $this->section_exists($sectionnumber);
339
 
340
        // We need to know the course format as the text strings depends on them.
341
        $courseformat = $this->get_course_format();
342
        if (get_string_manager()->string_exists('deletesection', $courseformat)) {
343
            $strdelete = get_string('deletesection', $courseformat);
344
        } else {
345
            $strdelete = get_string('deletesection');
346
        }
347
 
348
        // If javascript is on, link is inside a menu.
349
        if ($this->running_javascript()) {
350
            $this->i_open_section_edit_menu($sectionnumber);
351
        }
352
 
353
        // Click on delete link.
354
        $this->execute('behat_general::i_click_on_in_the',
355
            array($strdelete, "link", $this->escape($xpath), "xpath_element")
356
        );
357
 
358
    }
359
 
360
    /**
361
     * Turns course section highlighting on.
362
     *
363
     * @Given /^I turn section "(?P<section_number>\d+)" highlighting on$/
364
     * @param int $sectionnumber The section number
365
     */
366
    public function i_turn_section_highlighting_on($sectionnumber) {
367
 
368
        // Ensures the section exists.
369
        $xpath = $this->section_exists($sectionnumber);
370
 
371
        // If javascript is on, link is inside a menu.
372
        if ($this->running_javascript()) {
373
            $this->i_open_section_edit_menu($sectionnumber);
374
        }
375
 
376
        // Click on highlight topic link.
377
        $this->execute('behat_general::i_click_on_in_the',
378
            array(get_string('highlight'), "link", $this->escape($xpath), "xpath_element")
379
        );
380
    }
381
 
382
    /**
383
     * Turns course section highlighting off.
384
     *
385
     * @Given /^I turn section "(?P<section_number>\d+)" highlighting off$/
386
     * @param int $sectionnumber The section number
387
     */
388
    public function i_turn_section_highlighting_off($sectionnumber) {
389
 
390
        // Ensures the section exists.
391
        $xpath = $this->section_exists($sectionnumber);
392
 
393
        // If javascript is on, link is inside a menu.
394
        if ($this->running_javascript()) {
395
            $this->i_open_section_edit_menu($sectionnumber);
396
        }
397
 
398
        // Click on un-highlight topic link.
399
        $this->execute('behat_general::i_click_on_in_the',
400
            array(get_string('highlightoff'), "link", $this->escape($xpath), "xpath_element")
401
        );
402
    }
403
 
404
    /**
405
     * Shows the specified hidden section. You need to be in the course page and on editing mode.
406
     *
407
     * @Given /^I show section "(?P<section_number>\d+)"$/
408
     * @param int $sectionnumber
409
     */
410
    public function i_show_section($sectionnumber) {
411
        $showlink = $this->show_section_link_exists($sectionnumber);
412
 
413
        // Ensure section edit menu is open before interacting with it.
414
        if ($this->running_javascript()) {
415
            $this->i_open_section_edit_menu($sectionnumber);
416
        }
417
        $showlink->click();
418
 
419
        if ($this->running_javascript()) {
420
            $this->getSession()->wait(self::get_timeout() * 1000, self::PAGE_READY_JS);
421
            $this->i_wait_until_section_is_available($sectionnumber);
422
        }
423
    }
424
 
425
    /**
426
     * Hides the specified visible section. You need to be in the course page and on editing mode.
427
     *
428
     * @Given /^I hide section "(?P<section_number>\d+)"$/
429
     * @param int $sectionnumber
430
     */
431
    public function i_hide_section($sectionnumber) {
432
        // Ensures the section exists.
433
        $xpath = $this->section_exists($sectionnumber);
434
 
435
        // We need to know the course format as the text strings depends on them.
436
        $courseformat = $this->get_course_format();
437
        if (get_string_manager()->string_exists('hidefromothers', $courseformat)) {
438
            $strhide = get_string('hidefromothers', $courseformat);
439
        } else {
440
            $strhide = get_string('hidesection');
441
        }
442
 
443
        // If javascript is on, link is inside a menu.
444
        if ($this->running_javascript()) {
445
            $this->i_open_section_edit_menu($sectionnumber);
446
        }
447
 
448
        // Click on delete link.
449
        $this->execute('behat_general::i_click_on_in_the',
450
              array($strhide, "link", $this->escape($xpath), "xpath_element")
451
        );
452
 
453
        if ($this->running_javascript()) {
454
            $this->getSession()->wait(self::get_timeout() * 1000, self::PAGE_READY_JS);
455
            $this->i_wait_until_section_is_available($sectionnumber);
456
        }
457
    }
458
 
459
    /**
460
     * Go to editing section page for specified section number. You need to be in the course page and on editing mode.
461
     *
462
     * @Given /^I edit the section "(?P<section_number>\d+)"$/
463
     * @param int $sectionnumber
464
     */
465
    public function i_edit_the_section($sectionnumber) {
466
        // If javascript is on, link is inside a menu.
467
        if ($this->running_javascript()) {
468
            $this->i_open_section_edit_menu($sectionnumber);
469
        }
470
 
471
        // We need to know the course format as the text strings depends on them.
472
        $courseformat = $this->get_course_format();
473
        if (get_string_manager()->string_exists('editsection', $courseformat)) {
474
            $stredit = get_string('editsection', $courseformat);
475
        } else {
476
            $stredit = get_string('editsection');
477
        }
478
 
479
        // Click on un-highlight topic link.
480
        $this->execute('behat_general::i_click_on_in_the',
481
            array($stredit, "link", "#section-" . $sectionnumber . " .action-menu", "css_element")
482
        );
483
 
484
    }
485
 
486
    /**
487
     * Edit specified section and fill the form data with the specified field/value pairs.
488
     *
489
     * @When /^I edit the section "(?P<section_number>\d+)" and I fill the form with:$/
490
     * @param int $sectionnumber The section number
491
     * @param TableNode $data The activity field/value data
492
     */
493
    public function i_edit_the_section_and_i_fill_the_form_with($sectionnumber, TableNode $data) {
494
 
495
        // Edit given section.
496
        $this->execute("behat_course::i_edit_the_section", $sectionnumber);
497
 
498
        // Set form fields.
499
        $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
500
 
501
        // Save section settings.
502
        $this->execute("behat_forms::press_button", get_string('savechanges'));
503
    }
504
 
505
    /**
506
     * Checks if the specified course section hightlighting is turned on. You need to be in the course page on editing mode.
507
     *
508
     * @Then /^section "(?P<section_number>\d+)" should be highlighted$/
509
     * @throws ExpectationException
510
     * @param int $sectionnumber The section number
511
     */
512
    public function section_should_be_highlighted($sectionnumber) {
513
 
514
        // Ensures the section exists.
515
        $xpath = $this->section_exists($sectionnumber);
516
 
517
        $this->execute('behat_general::should_exist_in_the', ['Highlighted', 'text', $xpath, 'xpath_element']);
518
        // The important checking, we can not check the img.
519
        $this->execute('behat_general::should_exist_in_the', ['Unhighlight', 'link', $xpath, 'xpath_element']);
520
    }
521
 
522
    /**
523
     * Checks if the specified course section highlighting is turned off. You need to be in the course page on editing mode.
524
     *
525
     * @Then /^section "(?P<section_number>\d+)" should not be highlighted$/
526
     * @throws ExpectationException
527
     * @param int $sectionnumber The section number
528
     */
529
    public function section_should_not_be_highlighted($sectionnumber) {
530
 
531
        // We only catch ExpectationException, ElementNotFoundException should be thrown if the specified section does not exist.
532
        try {
533
            $this->section_should_be_highlighted($sectionnumber);
534
        } catch (ExpectationException $e) {
535
            // ExpectedException means that it is not highlighted.
536
            return;
537
        }
538
 
539
        throw new ExpectationException('The "' . $sectionnumber . '" section is highlighted', $this->getSession());
540
    }
541
 
542
    /**
543
     * Checks that the specified section is visible. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
544
     *
545
     * @Then /^section "(?P<section_number>\d+)" should be hidden$/
546
     * @throws ExpectationException
547
     * @throws ElementNotFoundException Thrown by behat_base::find
548
     * @param int $sectionnumber
549
     */
550
    public function section_should_be_hidden($sectionnumber) {
551
 
552
        $sectionxpath = $this->section_exists($sectionnumber);
553
 
554
        // Preventive in case there is any action in progress.
555
        // Adding it here because we are interacting (click) with
556
        // the elements, not necessary when we just find().
557
        $this->i_wait_until_section_is_available($sectionnumber);
558
 
559
        // Section should be hidden.
560
        $exception = new ExpectationException('The section is not hidden', $this->getSession());
561
        $this->find('xpath', $sectionxpath . "[contains(concat(' ', normalize-space(@class), ' '), ' hidden ')]", $exception);
562
    }
563
 
564
    /**
565
     * Checks that all actiities in the specified section are hidden. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
566
     *
567
     * @Then /^all activities in section "(?P<section_number>\d+)" should be hidden$/
568
     * @throws ExpectationException
569
     * @throws ElementNotFoundException Thrown by behat_base::find
570
     * @param int $sectionnumber
571
     */
572
    public function section_activities_should_be_hidden($sectionnumber) {
573
        $sectionxpath = $this->section_exists($sectionnumber);
574
 
575
        // Preventive in case there is any action in progress.
576
        // Adding it here because we are interacting (click) with
577
        // the elements, not necessary when we just find().
578
        $this->i_wait_until_section_is_available($sectionnumber);
579
 
580
        // The checking are different depending on user permissions.
581
        if ($this->is_course_editor()) {
582
 
583
            // The section must be hidden.
584
            $this->show_section_link_exists($sectionnumber);
585
 
586
            // If there are activities they should be hidden and the visibility icon should not be available.
587
            if ($activities = $this->get_section_activities($sectionxpath)) {
588
 
589
                $dimmedexception = new ExpectationException('There are activities that are not hidden', $this->getSession());
590
                foreach ($activities as $activity) {
591
                    // Hidden from students.
592
                    $this->find('named_partial', array('badge', get_string('hiddenfromstudents')), $dimmedexception, $activity);
593
                }
594
            }
595
        } else {
596
            // There shouldn't be activities.
597
            if ($this->get_section_activities($sectionxpath)) {
598
                throw new ExpectationException('There are activities in the section and they should be hidden', $this->getSession());
599
            }
600
        }
601
 
602
    }
603
 
604
    /**
605
     * Checks that the specified section is visible. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
606
     *
607
     * @Then /^section "(?P<section_number>\d+)" should be visible$/
608
     * @throws ExpectationException
609
     * @param int $sectionnumber
610
     */
611
    public function section_should_be_visible($sectionnumber) {
612
 
613
        $sectionxpath = $this->section_exists($sectionnumber);
614
 
615
        // Section should not be hidden.
616
        $xpath = $sectionxpath . "[not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]";
617
        if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
618
            throw new ExpectationException('The section is hidden', $this->getSession());
619
        }
620
 
621
        // Edit menu should be visible.
622
        if ($this->is_course_editor()) {
623
            $xpath = $sectionxpath .
624
                    "/descendant::div[contains(@class, 'section-actions')]" .
625
                    "/descendant::a[contains(@data-toggle, 'dropdown')]";
626
            if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
627
                throw new ExpectationException('The section edit menu is not available', $this->getSession());
628
            }
629
        }
630
    }
631
 
632
    /**
633
     * Moves up the specified section, this step only works with Javascript disabled. Editing mode should be on.
634
     *
635
     * @Given /^I move up section "(?P<section_number>\d+)"$/
636
     * @throws DriverException Step not available when Javascript is enabled
637
     * @param int $sectionnumber
638
     */
639
    public function i_move_up_section($sectionnumber) {
640
 
641
        if ($this->running_javascript()) {
642
            throw new DriverException('Move a section up step is not available with Javascript enabled');
643
        }
644
 
645
        // Ensures the section exists.
646
        $sectionxpath = $this->section_exists($sectionnumber);
647
 
648
        // If javascript is on, link is inside a menu.
649
        if ($this->running_javascript()) {
650
            $this->i_open_section_edit_menu($sectionnumber);
651
        }
652
 
653
        // Follows the link
654
        $moveuplink = $this->get_node_in_container('link', get_string('moveup'), 'xpath_element', $sectionxpath);
655
        $moveuplink->click();
656
    }
657
 
658
    /**
659
     * Moves down the specified section, this step only works with Javascript disabled. Editing mode should be on.
660
     *
661
     * @Given /^I move down section "(?P<section_number>\d+)"$/
662
     * @throws DriverException Step not available when Javascript is enabled
663
     * @param int $sectionnumber
664
     */
665
    public function i_move_down_section($sectionnumber) {
666
 
667
        if ($this->running_javascript()) {
668
            throw new DriverException('Move a section down step is not available with Javascript enabled');
669
        }
670
 
671
        // Ensures the section exists.
672
        $sectionxpath = $this->section_exists($sectionnumber);
673
 
674
        // If javascript is on, link is inside a menu.
675
        if ($this->running_javascript()) {
676
            $this->i_open_section_edit_menu($sectionnumber);
677
        }
678
 
679
        // Follows the link
680
        $movedownlink = $this->get_node_in_container('link', get_string('movedown'), 'xpath_element', $sectionxpath);
681
        $movedownlink->click();
682
    }
683
 
684
    /**
685
     * Checks that the specified activity is visible. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
686
     *
687
     * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be visible$/
688
     * @param string $activityname
689
     * @throws ExpectationException
690
     */
691
    public function activity_should_be_visible($activityname) {
692
 
693
        // The activity must exists and be visible.
694
        $activitynode = $this->get_activity_node($activityname);
695
 
696
        if ($this->is_course_editor()) {
697
 
698
            // The activity should not be hidden from students.
699
            try {
700
                $this->find('named_partial', array('badge', get_string('hiddenfromstudents')), null, $activitynode);
701
                throw new ExpectationException('"' . $activityname . '" is hidden', $this->getSession());
702
            } catch (ElementNotFoundException $e) {
703
                // All ok.
704
            }
705
 
706
            // Additional check if this is a teacher in editing mode.
707
            if ($this->is_editing_on()) {
708
                // The 'Hide' button should be available.
709
                $nohideexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' .
710
                    get_string('hide') . '" icon', $this->getSession());
711
                $this->find('named_partial', array('link', get_string('hide')), $nohideexception, $activitynode);
712
            }
713
        }
714
    }
715
 
716
    /**
717
     * Checks that the specified activity is visible. You need to be in the course page.
718
     * It can be used being logged as a student and as a teacher on editing mode.
719
     *
720
     * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be available but hidden from course page$/
721
     * @param string $activityname
722
     * @throws ExpectationException
723
     */
724
    public function activity_should_be_available_but_hidden_from_course_page($activityname) {
725
 
726
        if ($this->is_course_editor()) {
727
 
728
            // The activity must exists and be visible.
729
            $activitynode = $this->get_activity_node($activityname);
730
 
731
            // Should not have the "Hidden from students" badge.
732
            try {
733
                $this->find('named_partial', array('badge', get_string('hiddenfromstudents')), null, $activitynode);
734
                throw new ExpectationException('"' . $activityname . '" is hidden', $this->getSession());
735
            } catch (ElementNotFoundException $e) {
736
                // All ok.
737
            }
738
 
739
            // Should have the "Available but not shown on course page" badge.
740
            $exception = new ExpectationException('"' . $activityname . '" is not Available', $this->getSession());
741
            $this->find('named_partial', array('badge', get_string('hiddenoncoursepage')), $exception, $activitynode);
742
 
743
            // Additional check if this is a teacher in editing mode.
744
            if ($this->is_editing_on()) {
745
                // Also has either 'Hide' or 'Make unavailable' edit control.
746
                $nohideexception = new ExpectationException('"' . $activityname . '" has neither "' . get_string('hide') .
747
                    '" nor "' . get_string('makeunavailable') . '" icons', $this->getSession());
748
                try {
749
                    $this->find('named_partial', array('link', get_string('hide')), false, $activitynode);
750
                } catch (ElementNotFoundException $e) {
751
                    $this->find('named_partial', array('link', get_string('makeunavailable')), $nohideexception, $activitynode);
752
                }
753
            }
754
 
755
        } else {
756
 
757
            // Student should not see the activity at all.
758
            try {
759
                $this->get_activity_node($activityname);
760
                throw new ExpectationException('The "' . $activityname . '" should not appear', $this->getSession());
761
            } catch (ElementNotFoundException $e) {
762
                // This is good, the activity should not be there.
763
            }
764
        }
765
    }
766
 
767
    /**
768
     * Checks that the specified activity is hidden. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
769
     *
770
     * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be hidden$/
771
     * @param string $activityname
772
     * @throws ExpectationException
773
     */
774
    public function activity_should_be_hidden($activityname) {
775
        if ($this->is_course_editor()) {
776
            // The activity should exist.
777
            $activitynode = $this->get_activity_node($activityname);
778
 
779
            // Should be hidden.
780
            $exception = new ExpectationException('"' . $activityname . '" is not hidden', $this->getSession());
781
            $this->find('named_partial', array('badge', get_string('hiddenfromstudents')), $exception, $activitynode);
782
 
783
            // Additional check if this is a teacher in editing mode.
784
            if ($this->is_editing_on()) {
785
                // Also has either 'Show' or 'Make available' edit control.
786
                $noshowexception = new ExpectationException('"' . $activityname . '" has neither "' . get_string('show') .
787
                    '" nor "' . get_string('makeavailable') . '" icons', $this->getSession());
788
                try {
789
                    $this->find('named_partial', array('link', get_string('show')), false, $activitynode);
790
                } catch (ElementNotFoundException $e) {
791
                    $this->find('named_partial', array('link', get_string('makeavailable')), $noshowexception, $activitynode);
792
                }
793
            }
794
 
795
        } else {
796
            // It should not exist at all.
797
            try {
798
                $this->get_activity_node($activityname);
799
                throw new ExpectationException('The "' . $activityname . '" should not appear', $this->getSession());
800
            } catch (ElementNotFoundException $e) {
801
                // This is good, the activity should not be there.
802
            }
803
        }
804
 
805
    }
806
 
807
    /**
808
     * Checks that the specified label is hidden from students. You need to be in the course page.
809
     *
810
     * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" label should be hidden$/
811
     * @param string $activityname
812
     * @throws ExpectationException
813
     */
814
    public function label_should_be_hidden($activityname) {
815
        if ($this->is_course_editor()) {
816
            // The activity should exist.
817
            $activitynode = $this->get_activity_node($activityname);
818
 
819
            // Should be hidden.
820
            $exception = new ExpectationException('"' . $activityname . '" is not hidden', $this->getSession());
821
            $this->find('named_partial', array('badge', get_string('hiddenfromstudents')), $exception, $activitynode);
822
        }
823
    }
824
 
825
    /**
826
     * Moves the specified activity to the first slot of a section.
827
     *
828
     * Editing mode should be on.
829
     *
830
     * @Given /^I move "(?P<activity_name_string>(?:[^"]|\\")*)" activity to section "(?P<section_number>\d+)"$/
831
     * @param string $activityname The activity name
832
     * @param int $sectionnumber The number of section
833
     */
834
    public function i_move_activity_to_section($activityname, $sectionnumber): void {
835
        // Ensure the destination is valid.
836
        $sectionxpath = $this->section_exists($sectionnumber);
837
 
838
        // Not all formats are compatible with the move tool.
839
        $activitynode = $this->get_activity_node($activityname);
840
        if (!$activitynode->find('css', "[data-action='moveCm']", false, false, 0)) {
841
            // Execute the legacy YUI move option.
842
            $this->i_move_activity_to_section_yui($activityname, $sectionnumber);
843
            return;
844
        }
845
 
846
        // JS enabled.
847
        if ($this->running_javascript()) {
848
            $this->i_open_actions_menu($activityname);
849
            $this->execute(
850
                'behat_course::i_click_on_in_the_activity',
851
                [get_string('move'), "link", $this->escape($activityname)]
852
            );
853
            $this->execute("behat_general::i_click_on_in_the", [
854
                "[data-for='section'][data-number='$sectionnumber']",
855
                'css_element',
856
                "[data-region='modal-container']",
857
                'css_element'
858
            ]);
859
        } else {
860
            $this->execute(
861
                'behat_course::i_click_on_in_the_activity',
862
                [get_string('move'), "link", $this->escape($activityname)]
863
            );
864
            $this->execute(
865
                'behat_general::i_click_on_in_the',
866
                ["li.movehere a", "css_element", $this->escape($sectionxpath), "xpath_element"]
867
            );
868
        }
869
    }
870
 
871
    /**
872
     * Moves the specified activity to the first slot of a section using the YUI course format.
873
     *
874
     * This step is experimental when using it in Javascript tests. Editing mode should be on.
875
     *
876
     * @param string $activityname The activity name
877
     * @param int $sectionnumber The number of section
878
     */
879
    public function i_move_activity_to_section_yui($activityname, $sectionnumber): void {
880
        // Ensure the destination is valid.
881
        $sectionxpath = $this->section_exists($sectionnumber);
882
 
883
        // JS enabled.
884
        if ($this->running_javascript()) {
885
            $activitynode = $this->get_activity_element('Move', 'icon', $activityname);
886
            $destinationxpath = $sectionxpath . "/descendant::ul[contains(concat(' ', normalize-space(@class), ' '), ' yui3-dd-drop ')]";
887
            $this->execute(
888
                "behat_general::i_drag_and_i_drop_it_in",
889
                [
890
                    $this->escape($activitynode->getXpath()), "xpath_element",
891
                    $this->escape($destinationxpath), "xpath_element",
892
                ]
893
            );
894
        } else {
895
            // Following links with no-JS.
896
            // Moving to the fist spot of the section (before all other section's activities).
897
            $this->execute(
898
                'behat_course::i_click_on_in_the_activity',
899
                ["a.editing_move", "css_element", $this->escape($activityname)]
900
            );
901
            $this->execute(
902
                'behat_general::i_click_on_in_the',
903
                ["li.movehere a", "css_element", $this->escape($sectionxpath), "xpath_element"]
904
            );
905
        }
906
    }
907
 
908
    /**
909
     * Edits the activity name through the edit activity; this step only works with Javascript enabled. Editing mode should be on.
910
     *
911
     * @Given /^I change "(?P<activity_name_string>(?:[^"]|\\")*)" activity name to "(?P<new_name_string>(?:[^"]|\\")*)"$/
912
     * @throws DriverException Step not available when Javascript is disabled
913
     * @param string $activityname
914
     * @param string $newactivityname
915
     */
916
    public function i_change_activity_name_to($activityname, $newactivityname) {
917
        $this->execute('behat_forms::i_set_the_field_in_container_to', [
918
            get_string('edittitle'),
919
            $activityname,
920
            'activity',
921
            $newactivityname
922
        ]);
923
    }
924
 
925
    /**
926
     * Opens an activity actions menu if it is not already opened.
927
     *
928
     * @Given /^I open "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu$/
929
     * @throws DriverException The step is not available when Javascript is disabled
930
     * @param string $activityname
931
     */
932
    public function i_open_actions_menu($activityname) {
933
 
934
        if (!$this->running_javascript()) {
935
            throw new DriverException('Activities actions menu not available when Javascript is disabled');
936
        }
937
 
938
        // If it is already opened we do nothing.
939
        $activitynode = $this->get_activity_node($activityname);
940
 
941
        // Find the menu.
942
        $menunode = $activitynode->find('css', 'a[data-toggle=dropdown]');
943
        if (!$menunode) {
944
            throw new ExpectationException(sprintf('Could not find actions menu for the activity "%s"', $activityname),
945
                    $this->getSession());
946
        }
947
        $expanded = $menunode->getAttribute('aria-expanded');
948
        if ($expanded == 'true') {
949
            return;
950
        }
951
 
952
        $this->execute('behat_course::i_click_on_in_the_activity',
953
                array("a[data-toggle='dropdown']", "css_element", $this->escape($activityname))
954
        );
955
 
956
        $this->actions_menu_should_be_open($activityname);
957
    }
958
 
959
    /**
960
     * Closes an activity actions menu if it is not already closed.
961
     *
962
     * @Given /^I close "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu$/
963
     * @throws DriverException The step is not available when Javascript is disabled
964
     * @param string $activityname
965
     */
966
    public function i_close_actions_menu($activityname) {
967
 
968
        if (!$this->running_javascript()) {
969
            throw new DriverException('Activities actions menu not available when Javascript is disabled');
970
        }
971
 
972
        // If it is already closed we do nothing.
973
        $activitynode = $this->get_activity_node($activityname);
974
        // Find the menu.
975
        $menunode = $activitynode->find('css', 'a[data-toggle=dropdown]');
976
        if (!$menunode) {
977
            throw new ExpectationException(sprintf('Could not find actions menu for the activity "%s"', $activityname),
978
                    $this->getSession());
979
        }
980
        $expanded = $menunode->getAttribute('aria-expanded');
981
        if ($expanded != 'true') {
982
            return;
983
        }
984
 
985
        $this->execute('behat_course::i_click_on_in_the_activity',
986
                array("a[data-toggle='dropdown']", "css_element", $this->escape($activityname))
987
        );
988
    }
989
 
990
    /**
991
     * Checks that the specified activity's action menu is open.
992
     *
993
     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" actions menu should be open$/
994
     * @throws DriverException The step is not available when Javascript is disabled
995
     * @param string $activityname
996
     */
997
    public function actions_menu_should_be_open($activityname) {
998
 
999
        if (!$this->running_javascript()) {
1000
            throw new DriverException('Activities actions menu not available when Javascript is disabled');
1001
        }
1002
 
1003
        $activitynode = $this->get_activity_node($activityname);
1004
        // Find the menu.
1005
        $menunode = $activitynode->find('css', 'a[data-toggle=dropdown]');
1006
        if (!$menunode) {
1007
            throw new ExpectationException(sprintf('Could not find actions menu for the activity "%s"', $activityname),
1008
                    $this->getSession());
1009
        }
1010
        $expanded = $menunode->getAttribute('aria-expanded');
1011
        if ($expanded != 'true') {
1012
            throw new ExpectationException(sprintf("The action menu for '%s' is not open", $activityname), $this->getSession());
1013
        }
1014
    }
1015
 
1016
    /**
1017
     * Checks that the specified activity's action menu contains an item.
1018
     *
1019
     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" actions menu should have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
1020
     * @throws DriverException The step is not available when Javascript is disabled
1021
     * @param string $activityname
1022
     * @param string $menuitem
1023
     */
1024
    public function actions_menu_should_have_item($activityname, $menuitem) {
1025
        $activitynode = $this->get_activity_action_menu_node($activityname);
1026
 
1027
        $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' .
1028
            $menuitem . '" item', $this->getSession());
1029
        $this->find('named_partial', array('link', $menuitem), $notfoundexception, $activitynode);
1030
    }
1031
 
1032
    /**
1033
     * Checks that the specified activity's action menu does not contains an item.
1034
     *
1035
     * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" actions menu should not have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
1036
     * @throws DriverException The step is not available when Javascript is disabled
1037
     * @param string $activityname
1038
     * @param string $menuitem
1039
     */
1040
    public function actions_menu_should_not_have_item($activityname, $menuitem) {
1041
        $activitynode = $this->get_activity_action_menu_node($activityname);
1042
 
1043
        try {
1044
            $this->find('named_partial', array('link', $menuitem), false, $activitynode);
1045
            throw new ExpectationException('"' . $activityname . '" has a "' . $menuitem .
1046
                '" item when it should not', $this->getSession());
1047
        } catch (ElementNotFoundException $e) {
1048
            // This is good, the menu item should not be there.
1049
        }
1050
    }
1051
 
1052
    /**
1053
     * Returns the DOM node of the activity action menu.
1054
     *
1055
     * @throws ElementNotFoundException Thrown by behat_base::find
1056
     * @param string $activityname The activity name
1057
     * @return \Behat\Mink\Element\NodeElement
1058
     */
1059
    protected function get_activity_action_menu_node($activityname) {
1060
        $activityname = behat_context_helper::escape($activityname);
1061
        $xpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityname)]" .
1062
            "//div[contains(@class, 'action-menu')]";
1063
        return $this->find('xpath', $xpath);
1064
    }
1065
 
1066
    /**
1067
     * Indents to the right the activity or resource specified by it's name. Editing mode should be on.
1068
     *
1069
     * @Given /^I indent right "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1070
     * @param string $activityname
1071
     */
1072
    public function i_indent_right_activity($activityname) {
1073
 
1074
        $activity = $this->escape($activityname);
1075
        if ($this->running_javascript()) {
1076
            $this->i_open_actions_menu($activity);
1077
        }
1078
 
1079
        $this->execute('behat_course::i_click_on_in_the_activity',
1080
            array(get_string('moveright'), "link", $this->escape($activity))
1081
        );
1082
 
1083
    }
1084
 
1085
    /**
1086
     * Indents to the left the activity or resource specified by it's name. Editing mode should be on.
1087
     *
1088
     * @Given /^I indent left "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1089
     * @param string $activityname
1090
     */
1091
    public function i_indent_left_activity($activityname) {
1092
 
1093
        $activity = $this->escape($activityname);
1094
        if ($this->running_javascript()) {
1095
            $this->i_open_actions_menu($activity);
1096
        }
1097
 
1098
        $this->execute('behat_course::i_click_on_in_the_activity',
1099
            array(get_string('moveleft'), "link", $this->escape($activity))
1100
        );
1101
 
1102
    }
1103
 
1104
    /**
1105
     * Deletes the activity or resource specified by it's name. This step is experimental when using it in Javascript tests. You should be in the course page with editing mode on.
1106
     *
1107
     * @Given /^I delete "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1108
     * @param string $activityname
1109
     */
1110
    public function i_delete_activity($activityname) {
1111
        $steps = array();
1112
        $activity = $this->escape($activityname);
1113
        if ($this->running_javascript()) {
1114
            $this->i_open_actions_menu($activity);
1115
        }
1116
 
1117
        $this->execute('behat_course::i_click_on_in_the_activity',
1118
            array(get_string('delete'), "link", $this->escape($activity))
1119
        );
1120
 
1121
        // JS enabled.
1122
        // Not using chain steps here because the exceptions catcher have problems detecting
1123
        // JS modal windows and avoiding interacting them at the same time.
1124
        if ($this->running_javascript()) {
1125
            $this->execute(
1126
                'behat_general::i_click_on_in_the',
1127
                [
1128
                    get_string('delete'),
1129
                    "button",
1130
                    get_string('cmdelete_title', 'core_courseformat'),
1131
                    "dialogue"
1132
                ]
1133
            );
1134
        } else {
1135
            $this->execute("behat_forms::press_button", get_string('yes'));
1136
        }
1137
 
1138
        return $steps;
1139
    }
1140
 
1141
    /**
1142
     * Duplicates the activity or resource specified by it's name. You should be in the course page with editing mode on.
1143
     *
1144
     * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1145
     * @param string $activityname
1146
     */
1147
    public function i_duplicate_activity($activityname) {
1148
        $steps = array();
1149
        $activity = $this->escape($activityname);
1150
        if ($this->running_javascript()) {
1151
            $this->i_open_actions_menu($activity);
1152
        }
1153
        $this->execute('behat_course::i_click_on_in_the_activity',
1154
            array(get_string('duplicate'), "link", $activity)
1155
        );
1156
 
1157
    }
1158
 
1159
    /**
1160
     * Duplicates the activity or resource and modifies the new activity with the provided data. You should be in the course page with editing mode on.
1161
     *
1162
     * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity editing the new copy with:$/
1163
     * @param string $activityname
1164
     * @param TableNode $data
1165
     */
1166
    public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
1167
 
1168
        $activity = $this->escape($activityname);
1169
        $activityliteral = behat_context_helper::escape($activityname);
1170
 
1171
        $this->execute("behat_course::i_duplicate_activity", $activity);
1172
 
1173
        // Determine the future new activity xpath from the former one.
1174
        $duplicatedxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" .
1175
                "[contains(., $activityliteral)]/following-sibling::li";
1176
        $duplicatedactionsmenuxpath = $duplicatedxpath . "/descendant::a[@data-toggle='dropdown']";
1177
 
1178
        if ($this->running_javascript()) {
1179
            // We wait until the AJAX request finishes and the section is visible again.
1180
            $hiddenlightboxxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" .
1181
                    "[contains(., $activityliteral)]" .
1182
                    "/ancestor::li[contains(concat(' ', normalize-space(@class), ' '), ' section ')]" .
1183
                    "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]";
1184
 
1185
            // Component based courses do not use lightboxes anymore but js depending.
1186
            $sectionreadyxpath = "//*[contains(@id,'page-content')]" .
1187
                    "/descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' stateready ')]";
1188
 
1189
            $duplicationreadyxpath = "$hiddenlightboxxpath | $sectionreadyxpath";
1190
            $this->execute(
1191
                "behat_general::wait_until_exists",
1192
                [$this->escape($duplicationreadyxpath), "xpath_element"]
1193
            );
1194
 
1195
            // Close the original activity actions menu.
1196
            $this->i_close_actions_menu($activity);
1197
 
1198
            // The next sibling of the former activity will be the duplicated one, so we click on it from it's xpath as, at
1199
            // this point, it don't even exists in the DOM (the steps are executed when we return them).
1200
            $this->execute('behat_general::i_click_on',
1201
                    array($this->escape($duplicatedactionsmenuxpath), "xpath_element")
1202
            );
1203
        }
1204
 
1205
        // We force the xpath as otherwise mink tries to interact with the former one.
1206
        $this->execute('behat_general::i_click_on_in_the',
1207
                array(get_string('editsettings'), "link", $this->escape($duplicatedxpath), "xpath_element")
1208
        );
1209
 
1210
        $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
1211
        $this->execute("behat_forms::press_button", get_string('savechangesandreturntocourse'));
1212
 
1213
    }
1214
 
1215
    /**
1216
     * Waits until the section is available to interact with it. Useful when the section is performing an action and the section is overlayed with a loading layout.
1217
     *
1218
     * Using the protected method as this method will be usually
1219
     * called by other methods which are not returning a set of
1220
     * steps and performs the actions directly, so it would not
1221
     * be executed if it returns another step.
1222
     *
1223
     * Hopefully we would not require test writers to use this step
1224
     * and we will manage it from other step definitions.
1225
     *
1226
     * @Given /^I wait until section "(?P<section_number>\d+)" is available$/
1227
     * @param int $sectionnumber
1228
     * @return void
1229
     */
1230
    public function i_wait_until_section_is_available($sectionnumber) {
1231
 
1232
        // Looks for a hidden lightbox or a non-existent lightbox in that section.
1233
        $sectionxpath = $this->section_exists($sectionnumber);
1234
        $hiddenlightboxxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]" .
1235
            " | " .
1236
            $sectionxpath . "[count(child::div[contains(@class, 'lightbox')]) = 0]";
1237
 
1238
        $this->ensure_element_exists($hiddenlightboxxpath, 'xpath_element');
1239
    }
1240
 
1241
    /**
1242
     * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
1243
     *
1244
     * @Given /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" in the "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1245
     * @param string $element
1246
     * @param string $selectortype
1247
     * @param string $activityname
1248
     */
1249
    public function i_click_on_in_the_activity($element, $selectortype, $activityname) {
1250
        $element = $this->get_activity_element($element, $selectortype, $activityname);
1251
        $element->click();
1252
    }
1253
 
1254
    /**
1255
     * Clicks on the specified element inside the activity container.
1256
     *
1257
     * @throws ElementNotFoundException
1258
     * @param string $element
1259
     * @param string $selectortype
1260
     * @param string $activityname
1261
     * @return NodeElement
1262
     */
1263
    protected function get_activity_element($element, $selectortype, $activityname) {
1264
        $activitynode = $this->get_activity_node($activityname);
1265
 
1266
        $exception = new ElementNotFoundException($this->getSession(), "'{$element}' '{$selectortype}' in '{$activityname}'");
1267
        return $this->find($selectortype, $element, $exception, $activitynode);
1268
    }
1269
 
1270
    /**
1271
     * Checks if the course section exists.
1272
     *
1273
     * @throws ElementNotFoundException Thrown by behat_base::find
1274
     * @param int $sectionnumber
1275
     * @return string The xpath of the section.
1276
     */
1277
    protected function section_exists($sectionnumber) {
1278
 
1279
        // Just to give more info in case it does not exist.
1280
        $xpath = "//li[@id='section-" . $sectionnumber . "']";
1281
        $exception = new ElementNotFoundException($this->getSession(), "Section $sectionnumber ");
1282
        $this->find('xpath', $xpath, $exception);
1283
 
1284
        return $xpath;
1285
    }
1286
 
1287
    /**
1288
     * Returns the show section icon or throws an exception.
1289
     *
1290
     * @throws ElementNotFoundException Thrown by behat_base::find
1291
     * @param int $sectionnumber
1292
     * @return NodeElement
1293
     */
1294
    protected function show_section_link_exists($sectionnumber) {
1295
 
1296
        // Gets the section xpath and ensure it exists.
1297
        $xpath = $this->section_exists($sectionnumber);
1298
 
1299
        // We need to know the course format as the text strings depends on them.
1300
        $courseformat = $this->get_course_format();
1301
 
1302
        // Checking the show button alt text and show icon.
1303
        $showtext = get_string('showfromothers', $courseformat);
1304
        $linkxpath = $xpath . "//a[*[contains(text(), " . behat_context_helper::escape($showtext) . ")]]";
1305
 
1306
        $exception = new ElementNotFoundException($this->getSession(), 'Show section link');
1307
 
1308
        // Returing the link so both Non-JS and JS browsers can interact with it.
1309
        return $this->find('xpath', $linkxpath, $exception);
1310
    }
1311
 
1312
    /**
1313
     * Returns the hide section icon link if it exists or throws exception.
1314
     *
1315
     * @throws ElementNotFoundException Thrown by behat_base::find
1316
     * @param int $sectionnumber
1317
     * @return NodeElement
1318
     */
1319
    protected function hide_section_link_exists($sectionnumber) {
1320
 
1321
        // Gets the section xpath and ensure it exists.
1322
        $xpath = $this->section_exists($sectionnumber);
1323
 
1324
        // We need to know the course format as the text strings depends on them.
1325
        $courseformat = $this->get_course_format();
1326
 
1327
        // Checking the hide button alt text and hide icon.
1328
        $hidetext = behat_context_helper::escape(get_string('hidefromothers', $courseformat));
1329
        $linkxpath = $xpath . "/descendant::a[@title=$hidetext]";
1330
 
1331
        $exception = new ElementNotFoundException($this->getSession(), 'Hide section icon ');
1332
        $this->find('icon', 'Hide', $exception);
1333
 
1334
        // Returing the link so both Non-JS and JS browsers can interact with it.
1335
        return $this->find('xpath', $linkxpath, $exception);
1336
    }
1337
 
1338
    /**
1339
     * Gets the current course format.
1340
     *
1341
     * @throws ExpectationException If we are not in the course view page.
1342
     * @return string The course format in a frankenstyled name.
1343
     */
1344
    protected function get_course_format() {
1345
 
1346
        $exception = new ExpectationException('You are not in a course page', $this->getSession());
1347
 
1348
        // The moodle body's id attribute contains the course format.
1349
        $node = $this->getSession()->getPage()->find('css', 'body');
1350
        if (!$node) {
1351
            throw $exception;
1352
        }
1353
 
1354
        if (!$bodyid = $node->getAttribute('id')) {
1355
            throw $exception;
1356
        }
1357
 
1358
        if (strstr($bodyid, 'page-course-view-') === false) {
1359
            throw $exception;
1360
        }
1361
 
1362
        return 'format_' . str_replace('page-course-view-', '', $bodyid);
1363
    }
1364
 
1365
    /**
1366
     * Gets the section's activites DOM nodes.
1367
     *
1368
     * @param string $sectionxpath
1369
     * @return array NodeElement instances
1370
     */
1371
    protected function get_section_activities($sectionxpath) {
1372
 
1373
        $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]";
1374
 
1375
        // We spin here, as activities usually require a lot of time to load.
1376
        try {
1377
            $activities = $this->find_all('xpath', $xpath);
1378
        } catch (ElementNotFoundException $e) {
1379
            return false;
1380
        }
1381
 
1382
        return $activities;
1383
    }
1384
 
1385
    /**
1386
     * Returns the DOM node of the activity from <li>.
1387
     *
1388
     * @throws ElementNotFoundException Thrown by behat_base::find
1389
     * @param string $activityname The activity name
1390
     * @return NodeElement
1391
     */
1392
    protected function get_activity_node($activityname) {
1393
 
1394
        $activityname = behat_context_helper::escape($activityname);
1395
        $xpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityname)]";
1396
 
1397
        return $this->find('xpath', $xpath);
1398
    }
1399
 
1400
    /**
1401
     * Gets the activity instance name from the activity node.
1402
     *
1403
     * @throws ElementNotFoundException
1404
     * @param NodeElement $activitynode
1405
     * @return string
1406
     */
1407
    protected function get_activity_name($activitynode) {
1408
        $instancenamenode = $this->find('xpath', "//span[contains(concat(' ', normalize-space(@class), ' '), ' instancename ')]", false, $activitynode);
1409
        return $instancenamenode->getText();
1410
    }
1411
 
1412
    /**
1413
     * Returns whether the user can edit the course contents or not.
1414
     *
1415
     * @return bool
1416
     */
1417
    protected function is_course_editor(): bool {
1418
        try {
1419
            $this->find('field', get_string('editmode'), false, false, 0);
1420
            return true;
1421
        } catch (ElementNotFoundException $e) {
1422
            return false;
1423
        }
1424
    }
1425
 
1426
    /**
1427
     * Returns whether the user can edit the course contents and the editing mode is on.
1428
     *
1429
     * @return bool
1430
     */
1431
    protected function is_editing_on() {
1432
        $body = $this->find('xpath', "//body", false, false, 0);
1433
        return $body->hasClass('editing');
1434
    }
1435
 
1436
    /**
1437
     * Returns the category node from within the listing on the management page.
1438
     *
1439
     * @param string $idnumber
1440
     * @return \Behat\Mink\Element\NodeElement
1441
     */
1442
    protected function get_management_category_listing_node_by_idnumber($idnumber) {
1443
        $id = $this->get_category_id($idnumber);
1444
        $selector = sprintf('#category-listing .listitem-category[data-id="%d"] > div', $id);
1445
        return $this->find('css', $selector);
1446
    }
1447
 
1448
    /**
1449
     * Returns a category node from within the management interface.
1450
     *
1451
     * @param string $name The name of the category.
1452
     * @param bool $link If set to true we'll resolve to the link rather than just the node.
1453
     * @return \Behat\Mink\Element\NodeElement
1454
     */
1455
    protected function get_management_category_listing_node_by_name($name, $link = false) {
1456
        $selector = "//div[@id='category-listing']//li[contains(concat(' ', normalize-space(@class), ' '), ' listitem-category ')]//a[text()='{$name}']";
1457
        if ($link === false) {
1458
            $selector .= "/ancestor::li[@data-id][1]";
1459
        }
1460
        return $this->find('xpath', $selector);
1461
    }
1462
 
1463
    /**
1464
     * Returns a course node from within the management interface.
1465
     *
1466
     * @param string $name The name of the course.
1467
     * @param bool $link If set to true we'll resolve to the link rather than just the node.
1468
     * @return \Behat\Mink\Element\NodeElement
1469
     */
1470
    protected function get_management_course_listing_node_by_name($name, $link = false) {
1471
        $selector = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$name}']";
1472
        if ($link === false) {
1473
            $selector .= "/ancestor::li[@data-id]";
1474
        }
1475
        return $this->find('xpath', $selector);
1476
    }
1477
 
1478
    /**
1479
     * Returns the course node from within the listing on the management page.
1480
     *
1481
     * @param string $idnumber
1482
     * @return \Behat\Mink\Element\NodeElement
1483
     */
1484
    protected function get_management_course_listing_node_by_idnumber($idnumber) {
1485
        $id = $this->get_course_id($idnumber);
1486
        $selector = sprintf('#course-listing .listitem-course[data-id="%d"] > div', $id);
1487
        return $this->find('css', $selector);
1488
    }
1489
 
1490
    /**
1491
     * Clicks on a category in the management interface.
1492
     *
1493
     * @Given /^I click on category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1494
     * @param string $name
1495
     */
1496
    public function i_click_on_category_in_the_management_interface($name) {
1497
        $node = $this->get_management_category_listing_node_by_name($name, true);
1498
        $node->click();
1499
    }
1500
 
1501
    /**
1502
     * Clicks on a course in the management interface.
1503
     *
1504
     * @Given /^I click on course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1505
     * @param string $name
1506
     */
1507
    public function i_click_on_course_in_the_management_interface($name) {
1508
        $node = $this->get_management_course_listing_node_by_name($name, true);
1509
        $node->click();
1510
    }
1511
 
1512
    /**
1513
     * Clicks on a category checkbox in the management interface, if not checked.
1514
     *
1515
     * @Given /^I select category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1516
     * @param string $name
1517
     */
1518
    public function i_select_category_in_the_management_interface($name) {
1519
        $node = $this->get_management_category_listing_node_by_name($name);
1520
        $node = $node->findField('bcat[]');
1521
        if (!$node->isChecked()) {
1522
            $node->click();
1523
        }
1524
    }
1525
 
1526
    /**
1527
     * Clicks on a category checkbox in the management interface, if checked.
1528
     *
1529
     * @Given /^I unselect category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1530
     * @param string $name
1531
     */
1532
    public function i_unselect_category_in_the_management_interface($name) {
1533
        $node = $this->get_management_category_listing_node_by_name($name);
1534
        $node = $node->findField('bcat[]');
1535
        if ($node->isChecked()) {
1536
            $node->click();
1537
        }
1538
    }
1539
 
1540
    /**
1541
     * Clicks course checkbox in the management interface, if not checked.
1542
     *
1543
     * @Given /^I select course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1544
     * @param string $name
1545
     */
1546
    public function i_select_course_in_the_management_interface($name) {
1547
        $node = $this->get_management_course_listing_node_by_name($name);
1548
        $node = $node->findField('bc[]');
1549
        if (!$node->isChecked()) {
1550
            $node->click();
1551
        }
1552
    }
1553
 
1554
    /**
1555
     * Clicks course checkbox in the management interface, if checked.
1556
     *
1557
     * @Given /^I unselect course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1558
     * @param string $name
1559
     */
1560
    public function i_unselect_course_in_the_management_interface($name) {
1561
        $node = $this->get_management_course_listing_node_by_name($name);
1562
        $node = $node->findField('bc[]');
1563
        if ($node->isChecked()) {
1564
            $node->click();
1565
        }
1566
    }
1567
 
1568
    /**
1569
     * Move selected categories to top level in the management interface.
1570
     *
1571
     * @Given /^I move category "(?P<name_string>(?:[^"]|\\")*)" to top level in the management interface$/
1572
     * @param string $name
1573
     */
1574
    public function i_move_category_to_top_level_in_the_management_interface($name) {
1575
        $this->i_select_category_in_the_management_interface($name);
1576
 
1577
        $this->execute('behat_forms::i_set_the_field_to',
1578
            array('menumovecategoriesto', core_course_category::get(0)->get_formatted_name())
1579
        );
1580
 
1581
        // Save event.
1582
        $this->execute("behat_forms::press_button", "bulkmovecategories");
1583
    }
1584
 
1585
    /**
1586
     * Checks that a category is a subcategory of specific category.
1587
     *
1588
     * @Given /^I should see category "(?P<subcatidnumber_string>(?:[^"]|\\")*)" as subcategory of "(?P<catidnumber_string>(?:[^"]|\\")*)" in the management interface$/
1589
     * @throws ExpectationException
1590
     * @param string $subcatidnumber
1591
     * @param string $catidnumber
1592
     */
1593
    public function i_should_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber) {
1594
        $categorynodeid = $this->get_category_id($catidnumber);
1595
        $subcategoryid = $this->get_category_id($subcatidnumber);
1596
        $exception = new ExpectationException('The category '.$subcatidnumber.' is not a subcategory of '.$catidnumber, $this->getSession());
1597
        $selector = sprintf('#category-listing .listitem-category[data-id="%d"] .listitem-category[data-id="%d"]', $categorynodeid, $subcategoryid);
1598
        $this->find('css', $selector, $exception);
1599
    }
1600
 
1601
    /**
1602
     * Checks that a category is not a subcategory of specific category.
1603
     *
1604
     * @Given /^I should not see category "(?P<subcatidnumber_string>(?:[^"]|\\")*)" as subcategory of "(?P<catidnumber_string>(?:[^"]|\\")*)" in the management interface$/
1605
     * @throws ExpectationException
1606
     * @param string $subcatidnumber
1607
     * @param string $catidnumber
1608
     */
1609
    public function i_should_not_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber) {
1610
        try {
1611
            $this->i_should_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber);
1612
        } catch (ExpectationException $e) {
1613
            // ExpectedException means that it is not highlighted.
1614
            return;
1615
        }
1616
        throw new ExpectationException('The category '.$subcatidnumber.' is a subcategory of '.$catidnumber, $this->getSession());
1617
    }
1618
 
1619
    /**
1620
     * Click to expand a category revealing its sub categories within the management UI.
1621
     *
1622
     * @Given /^I click to expand category "(?P<idnumber_string>(?:[^"]|\\")*)" in the management interface$/
1623
     * @param string $idnumber
1624
     */
1625
    public function i_click_to_expand_category_in_the_management_interface($idnumber) {
1626
        $categorynode = $this->get_management_category_listing_node_by_idnumber($idnumber);
1627
        $exception = new ExpectationException('Category "' . $idnumber . '" does not contain an expand or collapse toggle.', $this->getSession());
1628
        $togglenode = $this->find('css', 'a[data-action=collapse],a[data-action=expand]', $exception, $categorynode);
1629
        $togglenode->click();
1630
    }
1631
 
1632
    /**
1633
     * Checks that a category within the management interface is visible.
1634
     *
1635
     * @Given /^category in management listing should be visible "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1636
     * @param string $idnumber
1637
     */
1638
    public function category_in_management_listing_should_be_visible($idnumber) {
1639
        $id = $this->get_category_id($idnumber);
1640
        $exception = new ExpectationException('The category '.$idnumber.' is not visible.', $this->getSession());
1641
        $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="1"]', $id);
1642
        $this->find('css', $selector, $exception);
1643
    }
1644
 
1645
    /**
1646
     * Checks that a category within the management interface is dimmed.
1647
     *
1648
     * @Given /^category in management listing should be dimmed "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1649
     * @param string $idnumber
1650
     */
1651
    public function category_in_management_listing_should_be_dimmed($idnumber) {
1652
        $id = $this->get_category_id($idnumber);
1653
        $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="0"]', $id);
1654
        $exception = new ExpectationException('The category '.$idnumber.' is visible.', $this->getSession());
1655
        $this->find('css', $selector, $exception);
1656
    }
1657
 
1658
    /**
1659
     * Checks that a course within the management interface is visible.
1660
     *
1661
     * @Given /^course in management listing should be visible "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1662
     * @param string $idnumber
1663
     */
1664
    public function course_in_management_listing_should_be_visible($idnumber) {
1665
        $id = $this->get_course_id($idnumber);
1666
        $exception = new ExpectationException('The course '.$idnumber.' is not visible.', $this->getSession());
1667
        $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="1"]', $id);
1668
        $this->find('css', $selector, $exception);
1669
    }
1670
 
1671
    /**
1672
     * Checks that a course within the management interface is dimmed.
1673
     *
1674
     * @Given /^course in management listing should be dimmed "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1675
     * @param string $idnumber
1676
     */
1677
    public function course_in_management_listing_should_be_dimmed($idnumber) {
1678
        $id = $this->get_course_id($idnumber);
1679
        $exception = new ExpectationException('The course '.$idnumber.' is visible.', $this->getSession());
1680
        $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="0"]', $id);
1681
        $this->find('css', $selector, $exception);
1682
    }
1683
 
1684
    /**
1685
     * Toggles the visibility of a course in the management UI.
1686
     *
1687
     * If it was visible it will be hidden. If it is hidden it will be made visible.
1688
     *
1689
     * @Given /^I toggle visibility of course "(?P<idnumber_string>(?:[^"]|\\")*)" in management listing$/
1690
     * @param string $idnumber
1691
     */
1692
    public function i_toggle_visibility_of_course_in_management_listing($idnumber) {
1693
        $id = $this->get_course_id($idnumber);
1694
        $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible]', $id);
1695
        $node = $this->find('css', $selector);
1696
        $exception = new ExpectationException('Course listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
1697
        if ($node->getAttribute('data-visible') === '1') {
1698
            $toggle = $this->find('css', '.action-hide', $exception, $node);
1699
        } else {
1700
            $toggle = $this->find('css', '.action-show', $exception, $node);
1701
        }
1702
        $toggle->click();
1703
    }
1704
 
1705
    /**
1706
     * Toggles the visibility of a category in the management UI.
1707
     *
1708
     * If it was visible it will be hidden. If it is hidden it will be made visible.
1709
     *
1710
     * @Given /^I toggle visibility of category "(?P<idnumber_string>(?:[^"]|\\")*)" in management listing$/
1711
     */
1712
    public function i_toggle_visibility_of_category_in_management_listing($idnumber) {
1713
        $id = $this->get_category_id($idnumber);
1714
        $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible]', $id);
1715
        $node = $this->find('css', $selector);
1716
        $exception = new ExpectationException('Category listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
1717
        if ($node->getAttribute('data-visible') === '1') {
1718
            $toggle = $this->find('css', '.action-hide', $exception, $node);
1719
        } else {
1720
            $toggle = $this->find('css', '.action-show', $exception, $node);
1721
        }
1722
        $toggle->click();
1723
    }
1724
 
1725
    /**
1726
     * Moves a category displayed in the management interface up or down one place.
1727
     *
1728
     * @Given /^I click to move category "(?P<idnumber_string>(?:[^"]|\\")*)" (?P<direction>up|down) one$/
1729
     *
1730
     * @param string $idnumber The category idnumber
1731
     * @param string $direction The direction to move in, either up or down
1732
     */
1733
    public function i_click_to_move_category_by_one($idnumber, $direction) {
1734
        $node = $this->get_management_category_listing_node_by_idnumber($idnumber);
1735
        $this->user_moves_listing_by_one('category', $node, $direction);
1736
    }
1737
 
1738
    /**
1739
     * Moves a course displayed in the management interface up or down one place.
1740
     *
1741
     * @Given /^I click to move course "(?P<idnumber_string>(?:[^"]|\\")*)" (?P<direction>up|down) one$/
1742
     *
1743
     * @param string $idnumber The course idnumber
1744
     * @param string $direction The direction to move in, either up or down
1745
     */
1746
    public function i_click_to_move_course_by_one($idnumber, $direction) {
1747
        $node = $this->get_management_course_listing_node_by_idnumber($idnumber);
1748
        $this->user_moves_listing_by_one('course', $node, $direction);
1749
    }
1750
 
1751
    /**
1752
     * Moves a course or category listing within the management interface up or down by one.
1753
     *
1754
     * @param string $listingtype One of course or category
1755
     * @param \Behat\Mink\Element\NodeElement $listingnode
1756
     * @param string $direction One of up or down.
1757
     * @param bool $highlight If set to false we don't check the node has been highlighted.
1758
     */
1759
    protected function user_moves_listing_by_one($listingtype, $listingnode, $direction, $highlight = true) {
1760
        $up = (strtolower($direction) === 'up');
1761
        if ($up) {
1762
            $exception = new ExpectationException($listingtype.' listing does not contain a moveup button.', $this->getSession());
1763
            $button = $this->find('css', 'a.action-moveup', $exception, $listingnode);
1764
        } else {
1765
            $exception = new ExpectationException($listingtype.' listing does not contain a movedown button.', $this->getSession());
1766
            $button = $this->find('css', 'a.action-movedown', $exception, $listingnode);
1767
        }
1768
        $button->click();
1769
        if ($this->running_javascript() && $highlight) {
1770
            $listitem = $listingnode->getParent();
1771
            $exception = new ExpectationException('Nothing was highlighted, ajax didn\'t occur or didn\'t succeed.', $this->getSession());
1772
            $this->spin(array($this, 'listing_is_highlighted'), $listitem->getTagName().'#'.$listitem->getAttribute('id'), 2, $exception, true);
1773
        }
1774
    }
1775
 
1776
    /**
1777
     * Used by spin to determine the callback has been highlighted.
1778
     *
1779
     * @param behat_course $self A self reference (default first arg from a spin callback)
1780
     * @param \Behat\Mink\Element\NodeElement $selector
1781
     * @return bool
1782
     */
1783
    protected function listing_is_highlighted($self, $selector) {
1784
        $listitem = $this->find('css', $selector);
1785
        return $listitem->hasClass('highlight');
1786
    }
1787
 
1788
    /**
1789
     * Check that one course appears before another in the course category management listings.
1790
     *
1791
     * @Given /^I should see course listing "(?P<preceedingcourse_string>(?:[^"]|\\")*)" before "(?P<followingcourse_string>(?:[^"]|\\")*)"$/
1792
     *
1793
     * @param string $preceedingcourse The first course to find
1794
     * @param string $followingcourse The second course to find (should be AFTER the first course)
1795
     * @throws ExpectationException
1796
     */
1797
    public function i_should_see_course_listing_before($preceedingcourse, $followingcourse) {
1798
        $xpath = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$preceedingcourse}']/ancestor::li[@data-id]//following::a[text()='{$followingcourse}']";
1799
        $msg = "{$preceedingcourse} course does not appear before {$followingcourse} course";
1800
        if (!$this->getSession()->getDriver()->find($xpath)) {
1801
            throw new ExpectationException($msg, $this->getSession());
1802
        }
1803
    }
1804
 
1805
    /**
1806
     * Check that one category appears before another in the course category management listings.
1807
     *
1808
     * @Given /^I should see category listing "(?P<preceedingcategory_string>(?:[^"]|\\")*)" before "(?P<followingcategory_string>(?:[^"]|\\")*)"$/
1809
     *
1810
     * @param string $preceedingcategory The first category to find
1811
     * @param string $followingcategory The second category to find (should be after the first category)
1812
     * @throws ExpectationException
1813
     */
1814
    public function i_should_see_category_listing_before($preceedingcategory, $followingcategory) {
1815
        $xpath = "//div[@id='category-listing']//li[contains(concat(' ', @class, ' '), ' listitem-category ')]//a[text()='{$preceedingcategory}']/ancestor::li[@data-id]//following::a[text()='{$followingcategory}']";
1816
        $msg = "{$preceedingcategory} category does not appear before {$followingcategory} category";
1817
        if (!$this->getSession()->getDriver()->find($xpath)) {
1818
            throw new ExpectationException($msg, $this->getSession());
1819
        }
1820
    }
1821
 
1822
    /**
1823
     * Checks that we are on the course management page that we expect to be on and that no course has been selected.
1824
     *
1825
     * @Given /^I should see the "(?P<mode_string>(?:[^"]|\\")*)" management page$/
1826
     * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
1827
     */
1828
    public function i_should_see_the_courses_management_page($mode) {
1829
        switch ($mode) {
1830
            case "Courses":
1831
                $heading = "Manage courses";
1832
                $this->execute("behat_general::should_not_exist", array("#category-listing", "css_element"));
1833
                $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1834
                break;
1835
 
1836
            case "Course categories":
1837
                $heading = "Manage course categories";
1838
                $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1839
                $this->execute("behat_general::should_not_exist", array("#course-listing", "css_element"));
1840
                break;
1841
 
1842
            case "Courses categories and courses":
1843
            default:
1844
                $heading = "Manage course categories and courses";
1845
                $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1846
                $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1847
                break;
1848
        }
1849
 
1850
        $this->execute("behat_general::assert_element_contains_text",
1851
            array($heading, "h2", "css_element")
1852
        );
1853
 
1854
        $this->execute("behat_general::should_not_exist", array("#course-detail", "css_element"));
1855
    }
1856
 
1857
    /**
1858
     * Checks that we are on the course management page that we expect to be on and that a course has been selected.
1859
     *
1860
     * @Given /^I should see the "(?P<mode_string>(?:[^"]|\\")*)" management page with a course selected$/
1861
     * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
1862
     */
1863
    public function i_should_see_the_courses_management_page_with_a_course_selected($mode) {
1864
        switch ($mode) {
1865
            case "Courses":
1866
                $heading = "Manage courses";
1867
                $this->execute("behat_general::should_not_exist", array("#category-listing", "css_element"));
1868
                $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1869
                break;
1870
 
1871
            case "Course categories":
1872
                $heading = "Manage course categories";
1873
                $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1874
                $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1875
                break;
1876
 
1877
            case "Courses categories and courses":
1878
            default:
1879
                $heading = "Manage course categories and courses";
1880
                $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1881
                $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1882
                break;
1883
        }
1884
 
1885
        $this->execute("behat_general::assert_element_contains_text",
1886
            array($heading, "h2", "css_element"));
1887
 
1888
        $this->execute("behat_general::should_exist", array("#course-detail", "css_element"));
1889
    }
1890
 
1891
    /**
1892
     * Locates a course in the course category management interface and then triggers an action for it.
1893
     *
1894
     * @Given /^I click on "(?P<action_string>(?:[^"]|\\")*)" action for "(?P<name_string>(?:[^"]|\\")*)" in management course listing$/
1895
     *
1896
     * @param string $action The action to take. One of
1897
     * @param string $name The name of the course as it is displayed in the management interface.
1898
     */
1899
    public function i_click_on_action_for_item_in_management_course_listing($action, $name) {
1900
        $node = $this->get_management_course_listing_node_by_name($name);
1901
        $this->user_clicks_on_management_listing_action('course', $node, $action);
1902
    }
1903
 
1904
    /**
1905
     * Locates a category in the course category management interface and then triggers an action for it.
1906
     *
1907
     * @Given /^I click on "(?P<action_string>(?:[^"]|\\")*)" action for "(?P<name_string>(?:[^"]|\\")*)" in management category listing$/
1908
     *
1909
     * @param string $action The action to take. One of
1910
     * @param string $name The name of the category as it is displayed in the management interface.
1911
     */
1912
    public function i_click_on_action_for_item_in_management_category_listing($action, $name) {
1913
        $node = $this->get_management_category_listing_node_by_name($name);
1914
        $this->user_clicks_on_management_listing_action('category', $node, $action);
1915
    }
1916
 
1917
    /**
1918
     * Clicks to expand or collapse a category displayed on the frontpage
1919
     *
1920
     * @Given /^I toggle "(?P<categoryname_string>(?:[^"]|\\")*)" category children visibility in frontpage$/
1921
     * @throws ExpectationException
1922
     * @param string $categoryname
1923
     */
1924
    public function i_toggle_category_children_visibility_in_frontpage($categoryname) {
1925
 
1926
        $headingtags = array();
1927
        for ($i = 1; $i <= 6; $i++) {
1928
            $headingtags[] = 'self::h' . $i;
1929
        }
1930
 
1931
        $exception = new ExpectationException('"' . $categoryname . '" category can not be found', $this->getSession());
1932
        $categoryliteral = behat_context_helper::escape($categoryname);
1933
        $xpath = "//div[@class='info']/descendant::*[" . implode(' or ', $headingtags) .
1934
            "][contains(@class,'categoryname')][./descendant::a[.=$categoryliteral]]";
1935
        $node = $this->find('xpath', $xpath, $exception);
1936
        $node->click();
1937
 
1938
        // Smooth expansion.
1939
        $this->getSession()->wait(1000);
1940
    }
1941
 
1942
    /**
1943
     * Finds the node to use for a management listitem action and clicks it.
1944
     *
1945
     * @param string $listingtype Either course or category.
1946
     * @param \Behat\Mink\Element\NodeElement $listingnode
1947
     * @param string $action The action being taken
1948
     * @throws Behat\Mink\Exception\ExpectationException
1949
     */
1950
    protected function user_clicks_on_management_listing_action($listingtype, $listingnode, $action) {
1951
        $actionsnode = $listingnode->find('xpath', "//*" .
1952
                "[contains(concat(' ', normalize-space(@class), ' '), '{$listingtype}-item-actions')]");
1953
        if (!$actionsnode) {
1954
            throw new ExpectationException("Could not find the actions for $listingtype", $this->getSession());
1955
        }
1956
        $actionnode = $actionsnode->find('css', '.action-'.$action);
1957
        if (!$actionnode) {
1958
            throw new ExpectationException("Expected action was not available or not found ($action)", $this->getSession());
1959
        }
1960
        if ($this->running_javascript() && !$actionnode->isVisible()) {
1961
            $actionsnode->find('css', 'a[data-toggle=dropdown]')->click();
1962
            $actionnode = $actionsnode->find('css', '.action-'.$action);
1963
        }
1964
        $actionnode->click();
1965
    }
1966
 
1967
    /**
1968
     * Clicks on a category in the management interface.
1969
     *
1970
     * @Given /^I click on "(?P<categoryname_string>(?:[^"]|\\")*)" category in the management category listing$/
1971
     * @param string $name The name of the category to click.
1972
     */
1973
    public function i_click_on_category_in_the_management_category_listing($name) {
1974
        $node = $this->get_management_category_listing_node_by_name($name);
1975
        $node->find('css', 'a.categoryname')->click();
1976
    }
1977
 
1978
    /**
1979
     * Locates a category in the course category management interface and then opens action menu for it.
1980
     *
1981
     * @Given /^I open the action menu for "(?P<name_string>(?:[^"]|\\")*)" in management category listing$/
1982
     *
1983
     * @param string $name The name of the category as it is displayed in the management interface.
1984
     */
1985
    public function i_open_the_action_menu_for_item_in_management_category_listing($name) {
1986
        $node = $this->get_management_category_listing_node_by_name($name);
1987
        $node->find('xpath', "//*[contains(@class, 'category-item-actions')]//a[@data-toggle='dropdown']")->click();
1988
    }
1989
 
1990
    /**
1991
     * Checks that the specified category actions menu contains an item.
1992
     *
1993
     * @Then /^"(?P<name_string>(?:[^"]|\\")*)" category actions menu should have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
1994
     *
1995
     * @param string $name
1996
     * @param string $menuitem
1997
     * @throws Behat\Mink\Exception\ExpectationException
1998
     */
1999
    public function category_actions_menu_should_have_item($name, $menuitem) {
2000
        $node = $this->get_management_category_listing_node_by_name($name);
2001
 
2002
        $notfoundexception = new ExpectationException('"' . $name . '" doesn\'t have a "' .
2003
            $menuitem . '" item', $this->getSession());
2004
        $this->find('named_partial', ['link', $menuitem], $notfoundexception, $node);
2005
    }
2006
 
2007
    /**
2008
     * Checks that the specified category actions menu does not contain an item.
2009
     *
2010
     * @Then /^"(?P<name_string>(?:[^"]|\\")*)" category actions menu should not have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
2011
     *
2012
     * @param string $name
2013
     * @param string $menuitem
2014
     * @throws Behat\Mink\Exception\ExpectationException
2015
     */
2016
    public function category_actions_menu_should_not_have_item($name, $menuitem) {
2017
        $node = $this->get_management_category_listing_node_by_name($name);
2018
 
2019
        try {
2020
            $this->find('named_partial', ['link', $menuitem], false, $node);
2021
            throw new ExpectationException('"' . $name . '" has a "' . $menuitem .
2022
                '" item when it should not', $this->getSession());
2023
        } catch (ElementNotFoundException $e) {
2024
            // This is good, the menu item should not be there.
2025
        }
2026
    }
2027
 
2028
    /**
2029
     * Go to the course participants
2030
     *
2031
     * @Given /^I navigate to course participants$/
2032
     */
2033
    public function i_navigate_to_course_participants() {
2034
        $this->execute('behat_navigation::i_select_from_secondary_navigation', get_string('participants'));
2035
    }
2036
 
2037
    /**
2038
     * Check that one teacher appears before another in the course contacts.
2039
     *
2040
     * @Given /^I should see teacher "(?P<pteacher_string>(?:[^"]|\\")*)" before "(?P<fteacher_string>(?:[^"]|\\")*)" in the course contact listing$/
2041
     *
2042
     * @param string $pteacher The first teacher to find
2043
     * @param string $fteacher The second teacher to find (should be after the first teacher)
2044
     *
2045
     * @throws ExpectationException
2046
     */
2047
    public function i_should_see_teacher_before($pteacher, $fteacher) {
2048
        $xpath = "//ul[contains(@class,'teachers')]//li//a[text()='{$pteacher}']/ancestor::li//following::a[text()='{$fteacher}']";
2049
        $msg = "Teacher {$pteacher} does not appear before Teacher {$fteacher}";
2050
        if (!$this->getSession()->getDriver()->find($xpath)) {
2051
            throw new ExpectationException($msg, $this->getSession());
2052
        }
2053
    }
2054
 
2055
    /**
2056
     * Check that one teacher oes not appears after another in the course contacts.
2057
     *
2058
     * @Given /^I should not see teacher "(?P<fteacher_string>(?:[^"]|\\")*)" after "(?P<pteacher_string>(?:[^"]|\\")*)" in the course contact listing$/
2059
     *
2060
     * @param string $fteacher The teacher that should not be found (after the other teacher)
2061
     * @param string $pteacher The teacher after who the other should not be found (this teacher must be found!)
2062
     *
2063
     * @throws ExpectationException
2064
     */
2065
    public function i_should_not_see_teacher_after($fteacher, $pteacher) {
2066
        $xpathliteral = behat_context_helper::escape($pteacher);
2067
        $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
2068
                "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
2069
        try {
2070
            $nodes = $this->find_all('xpath', $xpath);
2071
        } catch (ElementNotFoundException $e) {
2072
            throw new ExpectationException('"' . $pteacher . '" text was not found in the page', $this->getSession());
2073
        }
2074
        $xpath = "//ul[contains(@class,'teachers')]//li//a[text()='{$pteacher}']/ancestor::li//following::a[text()='{$fteacher}']";
2075
        $msg = "Teacher {$fteacher} appears after Teacher {$pteacher}";
2076
        if ($this->getSession()->getDriver()->find($xpath)) {
2077
            throw new ExpectationException($msg, $this->getSession());
2078
        }
2079
    }
2080
 
2081
    /**
2082
     * Open the activity chooser in a course.
2083
     *
2084
     * @Given /^I open the activity chooser$/
2085
     */
2086
    public function i_open_the_activity_chooser() {
2087
        $this->execute('behat_general::i_click_on',
2088
            array('//button[@data-action="open-chooser"]', 'xpath_element'));
2089
 
2090
        $node = $this->get_selected_node('xpath_element', '//div[@data-region="modules"]');
2091
        $this->ensure_node_is_visible($node);
2092
    }
2093
 
2094
    /**
2095
     * Checks the presence of the given text in the activity's displayed dates.
2096
     *
2097
     * @Given /^the activity date in "(?P<activityname>(?:[^"]|\\")*)" should contain "(?P<text>(?:[^"]|\\")*)"$/
2098
     * @param string $activityname The activity name.
2099
     * @param string $text The text to be searched in the activity date.
2100
     */
2101
    public function activity_date_in_activity_should_contain_text(string $activityname, string $text): void {
2102
        $containerselector = "//div[@data-activityname='$activityname']";
2103
        $containerselector .= "//div[@data-region='activity-dates']";
2104
 
2105
        $params = [$text, $containerselector, 'xpath_element'];
2106
        $this->execute("behat_general::assert_element_contains_text", $params);
2107
    }
2108
 
2109
    /**
2110
     * Checks the presence of activity dates information in the activity information output component.
2111
     *
2112
     * @Given /^the activity date information in "(?P<activityname>(?:[^"]|\\")*)" should exist$/
2113
     * @param string $activityname The activity name.
2114
     */
2115
    public function activity_dates_information_in_activity_should_exist(string $activityname): void {
2116
        $containerselector = "//div[@data-activityname='$activityname']";
2117
        $elementselector = "//div[@data-region='activity-dates']";
2118
        $params = [$elementselector, "xpath_element", $containerselector, "xpath_element"];
2119
        $this->execute("behat_general::should_exist_in_the", $params);
2120
    }
2121
 
2122
    /**
2123
     * Checks the absence of activity dates information in the activity information output component.
2124
     *
2125
     * @Given /^the activity date information in "(?P<activityname>(?:[^"]|\\")*)" should not exist$/
2126
     * @param string $activityname The activity name.
2127
     */
2128
    public function activity_dates_information_in_activity_should_not_exist(string $activityname): void {
2129
        $containerselector = "//div[@data-region='activity-information'][@data-activityname='$activityname']";
2130
        try {
2131
            $this->find('xpath_element', $containerselector);
2132
        } catch (ElementNotFoundException $e) {
2133
            // If activity information container does not exist (activity dates not shown, completion info not shown), all good.
2134
            return;
2135
        }
2136
 
2137
        // Otherwise, ensure that the completion information does not exist.
2138
        $elementselector = "//div[@data-region='activity-dates']";
2139
        $params = [$elementselector, "xpath_element", $containerselector, "xpath_element"];
2140
        $this->execute("behat_general::should_not_exist_in_the", $params);
2141
    }
2142
 
2143
    /**
2144
     * Get the section id from an identifier.
2145
     *
2146
     * The section name and summary are checked.
2147
     *
2148
     * @param string $courseidentifier
2149
     * @param string $sectionidentifier
2150
     * @return section_info|null section info or null if not found.
2151
     */
2152
    protected function get_section_and_course_by_id(string $courseidentifier, string $sectionidentifier): ?section_info {
2153
        $courseid = $this->get_course_id($courseidentifier);
2154
        if (!$courseid) {
2155
            return null;
2156
        }
2157
        $courseformat = course_get_format($courseid);
2158
        $sections = $courseformat->get_sections();
2159
        foreach ($sections as $section) {
2160
            $sectionfullname = $courseformat->get_section_name($section);
2161
            if ($section->name == $sectionidentifier
2162
                || $sectionfullname == $sectionidentifier
2163
            ) {
2164
                return $section;
2165
            }
2166
        }
2167
        return null;
2168
    }
2169
 
2170
    /**
2171
     * Get the section id from a courseid and a sectionnum.
2172
     *
2173
     * @param string $courseidentifier Course identifier.
2174
     * @param int $sectionnum Section number
2175
     * @return section_info|null section info or null if not found.
2176
     */
2177
    protected function get_section_and_course_by_sectionnum(string $courseidentifier, int $sectionnum): ?section_info {
2178
        $courseid = $this->get_course_id($courseidentifier);
2179
        if (!$courseid) {
2180
            return null;
2181
        }
2182
        $courseformat = course_get_format($courseid);
2183
        return $courseformat->get_section($sectionnum);
2184
    }
2185
}