Proyectos de Subversion Moodle

Rev

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