Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | 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
namespace core\navigation\views;
18
 
19
use booktool_print\output\renderer;
20
use navigation_node;
21
use ReflectionMethod;
22
use moodle_url;
23
 
24
/**
25
 * Class core_secondary_testcase
26
 *
27
 * Unit test for the secondary nav view.
28
 *
29
 * @package     core
30
 * @category    navigation
31
 * @copyright   2021 onwards Peter Dias
32
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33
 */
34
class secondary_test extends \advanced_testcase {
35
    /**
36
     * Test the get_leaf_nodes function
37
     * @param float $siteorder The order for the siteadmin node
38
     * @param float $courseorder The order for the course node
39
     * @param float $moduleorder The order for the module node
40
     * @dataProvider leaf_nodes_order_provider
41
     */
42
    public function test_get_leaf_nodes(float $siteorder, float $courseorder, float $moduleorder) {
43
        global $PAGE;
44
 
45
        // Create a secondary navigation and populate with some dummy nodes.
46
        $secondary = new secondary($PAGE);
47
        $secondary->add('Site Admin', '#', secondary::TYPE_SETTING, null, 'siteadmin');
48
        $secondary->add('Course Admin', '#', secondary::TYPE_CUSTOM, null, 'courseadmin');
49
        $secondary->add('Module Admin', '#', secondary::TYPE_SETTING, null, 'moduleadmin');
50
        $nodes = [
51
            navigation_node::TYPE_SETTING => [
52
                'siteadmin' => $siteorder,
53
                'moduleadmin' => $courseorder,
54
            ],
55
            navigation_node::TYPE_CUSTOM => [
56
                'courseadmin' => $moduleorder,
57
            ]
58
        ];
59
        $expectednodes = [
60
            "$siteorder" => 'siteadmin',
61
            "$courseorder" => 'moduleadmin',
62
            "$moduleorder" => 'courseadmin',
63
        ];
64
 
65
        $method = new ReflectionMethod('core\navigation\views\secondary', 'get_leaf_nodes');
66
        $sortednodes = $method->invoke($secondary, $secondary, $nodes);
67
        foreach ($sortednodes as $order => $node) {
68
            $this->assertEquals($expectednodes[$order], $node->key);
69
        }
70
    }
71
 
72
    /**
73
     * Data provider for test_get_leaf_nodes
74
     * @return array
75
     */
76
    public function leaf_nodes_order_provider(): array {
77
        return [
78
            'Initialise the order with whole numbers' => [3, 2, 1],
79
            'Initialise the order with a mix of whole and float numbers' => [2.1, 2, 1],
80
        ];
81
    }
82
 
83
    /**
84
     * Test the initialise in different contexts
85
     *
86
     * @param string $context The context to setup for - course, module, system
87
     * @param string $expectedfirstnode The expected first node
88
     * @param string $header The expected string
89
     * @param string $activenode The expected active node
90
     * @param string $courseformat The used course format (only applicable in the course and module context).
91
     * @return void
92
     * @dataProvider setting_initialise_provider
93
     */
94
    public function test_setting_initialise(string $context, string $expectedfirstnode,
95
            string $header, string $activenode, string $courseformat = 'topics'): void {
96
        global $PAGE, $SITE;
97
        $this->resetAfterTest();
98
        $this->setAdminUser();
99
        $pagecourse = $SITE;
100
        $pageurl = '/';
101
        switch ($context) {
102
            case 'course':
103
                $pagecourse = $this->getDataGenerator()->create_course(['format' => $courseformat]);
104
                $contextrecord = \context_course::instance($pagecourse->id, MUST_EXIST);
105
                if ($courseformat === 'singleactivity') {
106
                    $pageurl = new \moodle_url('/course/edit.php', ['id' => $pagecourse->id]);
107
                } else {
108
                    $pageurl = new \moodle_url('/course/view.php', ['id' => $pagecourse->id]);
109
                }
110
                break;
111
            case 'module':
112
                $pagecourse = $this->getDataGenerator()->create_course(['format' => $courseformat]);
113
                $assign = $this->getDataGenerator()->create_module('assign', ['course' => $pagecourse->id]);
114
                $cm = get_coursemodule_from_id('assign', $assign->cmid);
115
                $contextrecord = \context_module::instance($cm->id);
116
                $pageurl = new \moodle_url('/mod/assign/view.php', ['id' => $cm->id]);
117
                $PAGE->set_cm($cm);
118
                break;
119
            case 'system':
120
                $contextrecord = \context_system::instance();
121
                $PAGE->set_pagelayout('admin');
122
                $pageurl = new \moodle_url('/admin/index.php');
123
 
124
        }
125
        $PAGE->set_url($pageurl);
126
        navigation_node::override_active_url($pageurl);
127
        $PAGE->set_course($pagecourse);
128
        $PAGE->set_context($contextrecord);
129
 
130
        $node = new secondary($PAGE);
131
        $node->initialise();
132
        $children = $node->get_children_key_list();
133
        $this->assertEquals($expectedfirstnode, $children[0]);
134
        $this->assertEquals(get_string($header), $node->headertitle);
135
        $this->assertEquals($activenode, $node->activenode->text);
136
    }
137
 
138
    /**
139
     * Data provider for the test_setting_initialise function
140
     * @return array
141
     */
142
    public function setting_initialise_provider(): array {
143
        return [
144
            'Testing in a course context' => ['course', 'coursehome', 'courseheader', 'Course'],
145
            'Testing in a course context using a single activity course format' =>
146
                ['course', 'course', 'courseheader', 'Course', 'singleactivity'],
147
            'Testing in a module context' => ['module', 'modulepage', 'activityheader', 'Assignment'],
148
            'Testing in a module context using a single activity course format' =>
149
                ['module', 'course', 'activityheader', 'Activity', 'singleactivity'],
150
            'Testing in a site admin' => ['system', 'siteadminnode', 'homeheader', 'General'],
151
        ];
152
    }
153
 
154
    /**
155
     * Get the nav tree initialised to test active_node_scan.
156
     *
157
     * This is to test the secondary nav with navigation_node instance.
158
     *
159
     * @param string|null $seturl The url set for $PAGE.
160
     * @return navigation_node The initialised nav tree.
161
     */
162
    private function get_tree_initilised_to_set_activate(?string $seturl = null): navigation_node {
163
        global $PAGE;
164
 
165
        $node = new secondary($PAGE);
166
 
167
        $node->key = 'mytestnode';
168
        $node->type = navigation_node::TYPE_SYSTEM;
169
        $node->add('first child', null, navigation_node::TYPE_CUSTOM, 'firstchld', 'firstchild');
170
        $child2 = $node->add('second child', null, navigation_node::TYPE_COURSE, 'secondchld', 'secondchild');
171
        $child3 = $node->add('third child', null, navigation_node::TYPE_CONTAINER, 'thirdchld', 'thirdchild');
172
        $node->add('fourth child', null, navigation_node::TYPE_ACTIVITY, 'fourthchld', 'fourthchld');
173
        $node->add('fifth child', '/my', navigation_node::TYPE_CATEGORY, 'fifthchld', 'fifthchild');
174
 
175
        // If seturl is null then set actionurl of child6 to '/'.
176
        if ($seturl === null) {
177
            $child6actionurl = new \moodle_url('/');
178
        } else {
179
            // If seturl is provided then set actionurl of child6 to '/foo'.
180
            $child6actionurl = new \moodle_url('/foo');
181
        }
182
        $child6 = $child2->add('sixth child', $child6actionurl, navigation_node::TYPE_COURSE, 'sixthchld', 'sixthchild');
183
        // Activate the sixthchild node.
184
        $child6->make_active();
185
        $child2->add('seventh child', null, navigation_node::TYPE_COURSE, 'seventhchld', 'seventhchild');
186
        $child8 = $child2->add('eighth child', null, navigation_node::TYPE_CUSTOM, 'eighthchld', 'eighthchild');
187
        $child8->add('nineth child', null, navigation_node::TYPE_CUSTOM, 'ninethchld', 'ninethchild');
188
        $child3->add('tenth child', null, navigation_node::TYPE_CUSTOM, 'tenthchld', 'tenthchild');
189
 
190
        return $node;
191
    }
192
 
193
    /**
194
     * Testing active_node_scan on navigation_node instance.
195
     *
196
     * @param string $expectedkey The expected node key.
197
     * @param string|null $key The key set by user using set_secondary_active_tab.
198
     * @param string|null $seturl The url set by user.
199
     * @return void
200
     * @dataProvider active_node_scan_provider
201
     */
202
    public function test_active_node_scan(string $expectedkey, ?string $key = null, ?string $seturl = null): void {
203
        global $PAGE;
204
 
205
        if ($seturl !== null) {
206
            navigation_node::override_active_url(new \moodle_url($seturl));
207
        } else {
208
            $PAGE->set_url('/');
209
            navigation_node::override_active_url(new \moodle_url('/'));
210
        }
211
        if ($key !== null) {
212
            $PAGE->set_secondary_active_tab($key);
213
        }
214
 
215
        $node = $this->get_tree_initilised_to_set_activate($seturl);
216
        $secondary = new secondary($PAGE);
217
        $method = new ReflectionMethod('core\navigation\views\secondary', 'active_node_scan');
218
 
219
        $result = $method->invoke($secondary, $node);
220
 
221
        if ($expectedkey !== '') {
222
            $this->assertInstanceOf('navigation_node', $result);
223
            $this->assertEquals($expectedkey, $result->key);
224
        } else {
225
            $this->assertNull($result);
226
        }
227
    }
228
 
229
    /**
230
     * Data provider for the active_node_scan_provider
231
     *
232
     * @return array
233
     */
234
    public function active_node_scan_provider(): array {
235
        return [
236
            'Test by activating node adjacent to root node'
237
                => ['firstchild', 'firstchild'],
238
            'Activate a grand child node of the root'
239
                => ['thirdchild', 'tenthchild'],
240
            'When child node is activated the parent node is activated and returned'
241
                => ['secondchild', null],
242
            'Test by setting an empty string as node key to activate' => ['secondchild', ''],
243
            'Activate a node which does not exist in the tree'
244
                => ['', 'foobar'],
245
            'Activate the leaf node of the tree' => ['secondchild', 'ninethchild', null, true],
246
            'Changing the $PAGE url which is different from action url of child6 and not setting active tab manually'
247
                => ['', null, '/foobar'],
248
            'Having $PAGE url and child6 action url same and not setting active tab manually'
249
                => ['secondchild', null, '/foo'],
250
        ];
251
    }
252
 
253
    /**
254
     * Test the force_nodes_into_more_menu method.
255
     *
256
     * @param array $secondarynavnodesdata The array which contains the data used to generate the secondary navigation
257
     * @param array $defaultmoremenunodes  The array containing the keys of the navigation nodes which should be added
258
     *                                     to the "more" menu by default
259
     * @param int|null $maxdisplayednodes  The maximum limit of navigation nodes displayed in the secondary navigation
260
     * @param array $expecedmoremenunodes  The array containing the keys of the expected navigation nodes which are
261
     *                                     forced into the "more" menu
262
     * @dataProvider force_nodes_into_more_menu_provider
263
     */
264
    public function test_force_nodes_into_more_menu(array $secondarynavnodesdata, array $defaultmoremenunodes,
265
            ?int $maxdisplayednodes, array $expecedmoremenunodes) {
266
        global $PAGE;
267
 
268
        // Create a dummy secondary navigation.
269
        $secondary = new secondary($PAGE);
270
        foreach ($secondarynavnodesdata as $nodedata) {
271
            $secondary->add($nodedata['text'], '#', secondary::TYPE_SETTING, null, $nodedata['key']);
272
        }
273
 
274
        $method = new ReflectionMethod('core\navigation\views\secondary', 'force_nodes_into_more_menu');
275
        $method->invoke($secondary, $defaultmoremenunodes, $maxdisplayednodes);
276
 
277
        $actualmoremenunodes = [];
278
        foreach ($secondary->children as $node) {
279
            if ($node->forceintomoremenu) {
280
                $actualmoremenunodes[] = $node->key;
281
            }
282
        }
283
        // Assert that the actual nodes forced into the "more" menu matches the expected ones.
284
        $this->assertEquals($expecedmoremenunodes, $actualmoremenunodes);
285
    }
286
 
287
    /**
288
     * Data provider for the test_force_nodes_into_more_menu function.
289
     *
290
     * @return array
291
     */
292
    public function force_nodes_into_more_menu_provider(): array {
293
        return [
294
            'The total number of navigation nodes exceeds the max display limit (5); ' .
295
            'navnode2 and navnode4 are forced into "more" menu by default.' =>
296
                [
297
                    [
298
                        [ 'text' => 'Navigation node 1', 'key'  => 'navnode1'],
299
                        [ 'text' => 'Navigation node 2', 'key'  => 'navnode2'],
300
                        [ 'text' => 'Navigation node 3', 'key'  => 'navnode3'],
301
                        [ 'text' => 'Navigation node 4', 'key'  => 'navnode4'],
302
                        [ 'text' => 'Navigation node 5', 'key'  => 'navnode5'],
303
                        [ 'text' => 'Navigation node 6', 'key'  => 'navnode6'],
304
                        [ 'text' => 'Navigation node 7', 'key'  => 'navnode7'],
305
                        [ 'text' => 'Navigation node 8', 'key'  => 'navnode8'],
306
                        [ 'text' => 'Navigation node 9', 'key'  => 'navnode9'],
307
                    ],
308
                    [
309
                        'navnode2',
310
                        'navnode4',
311
                    ],
312
                    5,
313
                    [
314
                        'navnode2',
315
                        'navnode4',
316
                        'navnode8',
317
                        'navnode9',
318
                    ],
319
                ],
320
            'The total number of navigation nodes does not exceed the max display limit (5); ' .
321
            'navnode2 and navnode4 are forced into "more" menu by default.' =>
322
                [
323
                    [
324
                        [ 'text' => 'Navigation node 1', 'key'  => 'navnode1'],
325
                        [ 'text' => 'Navigation node 2', 'key'  => 'navnode2'],
326
                        [ 'text' => 'Navigation node 3', 'key'  => 'navnode3'],
327
                        [ 'text' => 'Navigation node 4', 'key'  => 'navnode4'],
328
                        [ 'text' => 'Navigation node 5', 'key'  => 'navnode5'],
329
                    ],
330
                    [
331
                        'navnode2',
332
                        'navnode4',
333
                    ],
334
                    5,
335
                    [
336
                        'navnode2',
337
                        'navnode4',
338
                    ],
339
                ],
340
            'The max display limit of navigation nodes is not defined; ' .
341
            'navnode2 and navnode4 are forced into "more" menu by default.' =>
342
                [
343
                    [
344
                        [ 'text' => 'Navigation node 1', 'key'  => 'navnode1'],
345
                        [ 'text' => 'Navigation node 2', 'key'  => 'navnode2'],
346
                        [ 'text' => 'Navigation node 3', 'key'  => 'navnode3'],
347
                        [ 'text' => 'Navigation node 4', 'key'  => 'navnode4'],
348
                        [ 'text' => 'Navigation node 5', 'key'  => 'navnode5'],
349
                    ],
350
                    [
351
                        'navnode2',
352
                        'navnode4',
353
                    ],
354
                    null,
355
                    [
356
                        'navnode2',
357
                        'navnode4',
358
                    ],
359
                ],
360
            'The total number of navigation nodes exceeds the max display limit (5); ' .
361
            'no forced navigation nodes into "more" menu by default.' =>
362
                [
363
                    [
364
                        [ 'text' => 'Navigation node 1', 'key'  => 'navnode1'],
365
                        [ 'text' => 'Navigation node 2', 'key'  => 'navnode2'],
366
                        [ 'text' => 'Navigation node 3', 'key'  => 'navnode3'],
367
                        [ 'text' => 'Navigation node 4', 'key'  => 'navnode4'],
368
                        [ 'text' => 'Navigation node 5', 'key'  => 'navnode5'],
369
                        [ 'text' => 'Navigation node 6', 'key'  => 'navnode6'],
370
                        [ 'text' => 'Navigation node 7', 'key'  => 'navnode7'],
371
                        [ 'text' => 'Navigation node 8', 'key'  => 'navnode8'],
372
                    ],
373
                    [],
374
                    5,
375
                    [
376
                        'navnode6',
377
                        'navnode7',
378
                        'navnode8',
379
                    ],
380
                ],
381
            'The total number of navigation nodes does not exceed the max display limit (5); ' .
382
            'no forced navigation nodes into "more" menu by default.' =>
383
                [
384
                    [
385
                        [ 'text' => 'Navigation node 1', 'key'  => 'navnode1'],
386
                        [ 'text' => 'Navigation node 2', 'key'  => 'navnode2'],
387
                        [ 'text' => 'Navigation node 3', 'key'  => 'navnode3'],
388
                        [ 'text' => 'Navigation node 4', 'key'  => 'navnode4'],
389
                        [ 'text' => 'Navigation node 5', 'key'  => 'navnode5'],
390
                        [ 'text' => 'Navigation node 6', 'key'  => 'navnode6'],
391
                    ],
392
                    [],
393
                    5,
394
                    [
395
                        'navnode6',
396
                    ],
397
                ],
398
            'The max display limit of navigation nodes is not defined; ' .
399
            'no forced navigation nodes into "more" menu by default.' =>
400
                [
401
                    [
402
                        [ 'text' => 'Navigation node 1', 'key'  => 'navnode1'],
403
                        [ 'text' => 'Navigation node 2', 'key'  => 'navnode2'],
404
                        [ 'text' => 'Navigation node 3', 'key'  => 'navnode3'],
405
                        [ 'text' => 'Navigation node 4', 'key'  => 'navnode4'],
406
                        [ 'text' => 'Navigation node 5', 'key'  => 'navnode5'],
407
                        [ 'text' => 'Navigation node 6', 'key'  => 'navnode6'],
408
                    ],
409
                    [],
410
                    null,
411
                    [],
412
                ],
413
        ];
414
    }
415
 
416
    /**
417
     * Recursive call to generate a navigation node given an array definition.
418
     *
419
     * @param array $structure
420
     * @param string $parentkey
421
     * @return navigation_node
422
     */
423
    private function generate_node_tree_construct(array $structure, string $parentkey): navigation_node {
424
        $node = navigation_node::create($parentkey, null, navigation_node::TYPE_CUSTOM, '', $parentkey);
425
        foreach ($structure as $key => $value) {
426
            if (is_array($value)) {
427
                $children = $value['children'] ?? $value;
428
                $child = $this->generate_node_tree_construct($children, $key);
429
                if (isset($value['action'])) {
430
                    $child->action = new \moodle_url($value['action']);
431
                }
432
                $node->add_node($child);
433
            } else {
434
                $node->add($key, $value, navigation_node::TYPE_CUSTOM, '', $key);
435
            }
436
        }
437
 
438
        return $node;
439
    }
440
 
441
    /**
442
     * Test the nodes_match_current_url function.
443
     *
444
     * @param string $selectedurl
445
     * @param string $expectednode
446
     * @dataProvider nodes_match_current_url_provider
447
     */
448
    public function test_nodes_match_current_url(string $selectedurl, string $expectednode) {
449
        global $PAGE;
450
        $structure = [
451
            'parentnode1' => [
452
                'child1' => '/my',
453
                'child2' => [
454
                    'child2.1' => '/view/course.php',
455
                    'child2.2' => '/view/admin.php',
456
                ]
457
            ]
458
        ];
459
        $node = $this->generate_node_tree_construct($structure, 'primarynode');
460
        $node->action = new \moodle_url('/');
461
 
462
        $PAGE->set_url($selectedurl);
463
        $secondary = new secondary($PAGE);
464
        $method = new ReflectionMethod('core\navigation\views\secondary', 'nodes_match_current_url');
465
        $response = $method->invoke($secondary, $node);
466
 
467
        $this->assertSame($response->key ?? null, $expectednode);
468
    }
469
 
470
    /**
471
     * Provider for test_nodes_match_current_url
472
     *
473
     * @return \string[][]
474
     */
475
    public function nodes_match_current_url_provider(): array {
476
        return [
477
            "Match url to a node that is a deep nested" => [
478
                '/view/course.php',
479
                'child2.1',
480
            ],
481
            "Match url to a parent node with children" => [
482
                '/', 'primarynode'
483
            ],
484
            "Match url to a child node" => [
485
                '/my', 'child1'
486
            ],
487
        ];
488
    }
489
 
490
    /**
491
     * Test the get_menu_array function
492
     *
493
     * @param string $selected
494
     * @param array $expected
495
     * @dataProvider get_menu_array_provider
496
     */
497
    public function test_get_menu_array(string $selected, array $expected) {
498
        global $PAGE;
499
 
500
        // Custom nodes - mimicing nodes added via 3rd party plugins.
501
        $structure = [
502
            'parentnode1' => [
503
                'child1' => '/my',
504
                'child2' => [
505
                    'action' => '/test.php',
506
                    'children' => [
507
                        'child2.1' => '/view/course.php?child=2',
508
                        'child2.2' => '/view/admin.php?child=2',
509
                        'child2.3' => '/test.php',
510
                    ]
511
                ],
512
                'child3' => [
513
                    'child3.1' => '/view/course.php?child=3',
514
                    'child3.2' => '/view/admin.php?child=3',
515
                ]
516
            ],
517
            'parentnode2' => "/view/module.php"
518
        ];
519
 
520
        $secondary = new secondary($PAGE);
521
        $secondary->add_node($this->generate_node_tree_construct($structure, 'primarynode'));
522
        $selectednode = $secondary->find($selected, null);
523
        $response = \core\navigation\views\secondary::create_menu_element([$selectednode]);
524
 
525
        $this->assertSame($expected, $response);
526
    }
527
 
528
    /**
529
     * Provider for test_get_menu_array
530
     *
531
     * @return array[]
532
     */
533
    public function get_menu_array_provider(): array {
534
        return [
535
            "Fetch information from a node with action and no children" => [
536
                'child1',
537
                [
538
                    'https://www.example.com/moodle/my' => 'child1'
539
                ],
540
            ],
541
            "Fetch information from a node with no action and children" => [
542
                'child3',
543
                [
544
                    'https://www.example.com/moodle/view/course.php?child=3' => 'child3.1',
545
                    'https://www.example.com/moodle/view/admin.php?child=3' => 'child3.2'
546
                ],
547
            ],
548
            "Fetch information from a node with children" => [
549
                'child2',
550
                [
551
                    'https://www.example.com/moodle/test.php' => 'child2.3',
552
                    'https://www.example.com/moodle/view/course.php?child=2' => 'child2.1',
553
                    'https://www.example.com/moodle/view/admin.php?child=2' => 'child2.2'
554
                ],
555
            ],
556
            "Fetch information from a node with an action and no children" => [
557
                'parentnode2',
558
                ['https://www.example.com/moodle/view/module.php' => 'parentnode2'],
559
            ],
560
            "Fetch information from a node with an action and multiple nested children" => [
561
                'parentnode1',
562
                [
563
                    [
564
                        'parentnode1' => [
565
                            'https://www.example.com/moodle/my' => 'child1'
566
                        ],
567
                        'child2' => [
568
                            'https://www.example.com/moodle/test.php' => 'child2',
569
                            'https://www.example.com/moodle/view/course.php?child=2' => 'child2.1',
570
                            'https://www.example.com/moodle/view/admin.php?child=2' => 'child2.2',
571
                        ],
572
                        'child3' => [
573
                            'https://www.example.com/moodle/view/course.php?child=3' => 'child3.1',
574
                            'https://www.example.com/moodle/view/admin.php?child=3' => 'child3.2'
575
                        ]
576
                    ]
577
                ],
578
            ],
579
        ];
580
    }
581
 
582
    /**
583
     * Test the get_node_with_first_action function
584
     *
585
     * @param string $selectedkey
586
     * @param string|null $expectedkey
587
     * @dataProvider get_node_with_first_action_provider
588
     */
589
    public function test_get_node_with_first_action(string $selectedkey, ?string $expectedkey) {
590
        global $PAGE;
591
        $structure = [
592
            'parentnode1' => [
593
                'child1' => [
594
                    'child1.1' => null
595
                ],
596
                'child2' => [
597
                    'child2.1' => [
598
                        'child2.1.1' => [
599
                            'action' => '/test.php',
600
                            'children' => [
601
                                'child2.1.1.1' => '/view/course.php?child=2',
602
                                'child2.1.1.2' => '/view/admin.php?child=2',
603
                            ]
604
                        ]
605
                    ]
606
                ],
607
                'child3' => [
608
                    'child3.1' => '/view/course.php?child=3',
609
                    'child3.2' => '/view/admin.php?child=3',
610
                ]
611
            ],
612
            'parentnode2' => "/view/module.php"
613
        ];
614
 
615
        $nodes = $this->generate_node_tree_construct($structure, 'primarynode');
616
        $selectednode = $nodes->find($selectedkey, null);
617
 
618
        $expected = null;
619
        // Expected response will be the parent node with the action updated.
620
        if ($expectedkey) {
621
            $expectedbasenode = clone $selectednode;
622
            $actionfromnode = $nodes->find($expectedkey, null);
623
            $expectedbasenode->action = $actionfromnode->action;
624
            $expected = $expectedbasenode;
625
        }
626
 
627
        $secondary = new secondary($PAGE);
628
        $method = new ReflectionMethod('core\navigation\views\secondary', 'get_node_with_first_action');
629
        $response = $method->invoke($secondary, $selectednode, $selectednode);
630
        $this->assertEquals($expected, $response);
631
    }
632
 
633
    /**
634
     * Provider for test_get_node_with_first_action
635
     *
636
     * @return array
637
     */
638
    public function get_node_with_first_action_provider(): array {
639
        return [
640
            "Search for action when parent has no action and multiple children with actions" => [
641
                "child3",
642
                "child3.1",
643
            ],
644
            "Search for action when parent child is deeply nested." => [
645
                "child2",
646
                "child2.1.1"
647
            ],
648
            "No navigation node returned when node has no children" => [
649
                "parentnode2",
650
                null
651
            ],
652
            "No navigation node returned when node has children but no actions available." => [
653
                "child1",
654
                null
655
            ],
656
        ];
657
    }
658
 
659
    /**
660
     * Test the add_external_nodes_to_secondary function.
661
     *
662
     * @param array $structure The structure of the navigation node tree to setup with.
663
     * @param array $expectednodes The expected nodes added to the secondary navigation
664
     * @param bool $separatenode Whether or not to create a separate node to add nodes to.
665
     * @dataProvider add_external_nodes_to_secondary_provider
666
     */
667
    public function test_add_external_nodes_to_secondary(array $structure, array $expectednodes, bool $separatenode = false) {
668
        global $PAGE;
669
 
670
        $this->resetAfterTest();
671
        $course = $this->getDataGenerator()->create_course();
672
        $context = \context_course::instance($course->id);
673
        $PAGE->set_context($context);
674
        $PAGE->set_url('/');
675
 
676
        $node = $this->generate_node_tree_construct($structure, 'parentnode');
677
        $secondary = new secondary($PAGE);
678
        $secondary->add_node($node);
679
        $firstnode = $node->get('parentnode1');
680
        $customparent = null;
681
        if ($separatenode) {
682
            $customparent = navigation_node::create('Custom parent');
683
        }
684
 
685
        $method = new ReflectionMethod('core\navigation\views\secondary', 'add_external_nodes_to_secondary');
686
        $method->invoke($secondary, $firstnode, $firstnode, $customparent);
687
 
688
        $actualnodes = $separatenode ? $customparent->get_children_key_list() : $secondary->get_children_key_list();
689
        $this->assertEquals($expectednodes, $actualnodes);
690
    }
691
 
692
    /**
693
     * Provider for the add_external_nodes_to_secondary function.
694
     *
695
     * @return array
696
     */
697
    public function add_external_nodes_to_secondary_provider() {
698
        return [
699
            "Container node with internal action and external children" => [
700
                [
701
                    'parentnode1' => [
702
                        'action' => '/test.php',
703
                        'children' => [
704
                            'child2.1' => 'https://example.org',
705
                            'child2.2' => 'https://example.net',
706
                        ]
707
                    ]
708
                ],
709
                ['parentnode', 'parentnode1']
710
            ],
711
            "Container node with external action and external children" => [
712
                [
713
                    'parentnode1' => [
714
                        'action' => 'https://example.com',
715
                        'children' => [
716
                            'child2.1' => 'https://example.org',
717
                            'child2.2' => 'https://example.net',
718
                        ]
719
                    ]
720
                ],
721
                ['parentnode', 'parentnode1', 'child2.1', 'child2.2']
722
            ],
723
            "Container node with external action and internal children" => [
724
                [
725
                    'parentnode1' => [
726
                        'action' => 'https://example.org',
727
                        'children' => [
728
                            'child2.1' => '/view/course.php',
729
                            'child2.2' => '/view/admin.php',
730
                        ]
731
                    ]
732
                ],
733
                ['parentnode', 'parentnode1', 'child2.1', 'child2.2']
734
            ],
735
            "Container node with internal actions and internal children" => [
736
                [
737
                    'parentnode1' => [
738
                        'action' => '/test.php',
739
                        'children' => [
740
                            'child2.1' => '/course.php',
741
                            'child2.2' => '/admin.php',
742
                        ]
743
                    ]
744
                ],
745
                ['parentnode', 'parentnode1']
746
            ],
747
            "Container node with internal action and external children adding to custom node" => [
748
                [
749
                    'parentnode1' => [
750
                        'action' => '/test.php',
751
                        'children' => [
752
                            'child2.1' => 'https://example.org',
753
                            'child2.2' => 'https://example.net',
754
                        ]
755
                    ]
756
                ],
757
                ['parentnode1'], true
758
            ],
759
            "Container node with external action and external children adding to custom node" => [
760
                [
761
                    'parentnode1' => [
762
                        'action' => 'https://example.com',
763
                        'children' => [
764
                            'child2.1' => 'https://example.org',
765
                            'child2.2' => 'https://example.net',
766
                        ]
767
                    ]
768
                ],
769
                ['parentnode1', 'child2.1', 'child2.2'], true
770
            ],
771
            "Container node with external action and internal children adding to custom node" => [
772
                [
773
                    'parentnode1' => [
774
                        'action' => 'https://example.org',
775
                        'children' => [
776
                            'child2.1' => '/view/course.php',
777
                            'child2.2' => '/view/admin.php',
778
                        ]
779
                    ]
780
                ],
781
                ['parentnode1', 'child2.1', 'child2.2'], true
782
            ],
783
            "Container node with internal actions and internal children adding to custom node" => [
784
                [
785
                    'parentnode1' => [
786
                        'action' => '/test.php',
787
                        'children' => [
788
                            'child2.1' => '/course.php',
789
                            'child2.2' => '/admin.php',
790
                        ]
791
                    ]
792
                ],
793
                ['parentnode1'], true
794
            ],
795
        ];
796
    }
797
 
798
    /**
799
     * Test the get_overflow_menu_data function
800
     *
801
     * @param string $selectedurl
802
     * @param bool $expectednull
803
     * @param bool $emptynode
804
     * @dataProvider get_overflow_menu_data_provider
805
     */
806
    public function test_get_overflow_menu_data(string $selectedurl, bool $expectednull, bool $emptynode = false) {
807
        global $PAGE;
808
 
809
        $this->resetAfterTest();
810
        // Custom nodes - mimicing nodes added via 3rd party plugins.
811
        $structure = [
812
            'parentnode1' => [
813
                'child1' => '/my',
814
                'child2' => [
815
                    'action' => '/test.php',
816
                    'children' => [
817
                        'child2.1' => '/view/course.php',
818
                        'child2.2' => '/view/admin.php',
819
                    ]
820
                ]
821
            ],
822
            'parentnode2' => "/view/module.php"
823
        ];
824
 
825
        $course = $this->getDataGenerator()->create_course();
826
        $context = \context_course::instance($course->id);
827
        $PAGE->set_context($context);
828
 
829
        $PAGE->set_url($selectedurl);
830
        navigation_node::override_active_url(new \moodle_url($selectedurl));
831
        $node = $this->generate_node_tree_construct($structure, 'primarynode');
832
        $node->action = new \moodle_url('/');
833
 
834
        $secondary = new secondary($PAGE);
835
        $secondary->add_node($node);
836
        $PAGE->settingsnav->add_node(clone $node);
837
        $secondary->add('Course home', '/coursehome.php', navigation_node::TYPE_CUSTOM, '', 'coursehome');
838
        $secondary->add('Course settings', '/course/settings.php', navigation_node::TYPE_CUSTOM, '', 'coursesettings');
839
 
840
        // Test for an empty node without children and action.
841
        if ($emptynode) {
842
            $node = $secondary->add('Course management', null, navigation_node::TYPE_CUSTOM, '', 'course');
843
            $node->make_active();
844
        } else {
845
            // Set the correct node as active.
846
            $method = new ReflectionMethod('core\navigation\views\secondary', 'scan_for_active_node');
847
            $method->invoke($secondary, $secondary);
848
        }
849
 
850
        $method = new ReflectionMethod('core\navigation\views\secondary', 'get_overflow_menu_data');
851
        $response = $method->invoke($secondary);
852
        if ($expectednull) {
853
            $this->assertNull($response);
854
        } else {
855
            $this->assertIsObject($response);
856
            $this->assertInstanceOf('url_select', $response);
857
        }
858
    }
859
 
860
    /**
861
     * Data provider for test_get_overflow_menu_data
862
     *
863
     * @return string[]
864
     */
865
    public function get_overflow_menu_data_provider(): array {
866
        return [
867
            "Active node is the course home node" => [
868
                '/coursehome.php',
869
                true
870
            ],
871
            "Active node is one with an action and no children" => [
872
                '/view/module.php',
873
                false
874
            ],
875
            "Active node is one with an action and children" => [
876
                '/',
877
                false
878
            ],
879
            "Active node is one without an action and children" => [
880
                '/',
881
                true,
882
                true,
883
            ],
884
            "Active node is one with an action and children but is NOT in settingsnav" => [
885
                '/course/settings.php',
886
                true
887
            ],
888
        ];
889
    }
890
 
891
    /**
892
     * Test the course administration settings return an overflow menu.
893
     *
894
     * @dataProvider get_overflow_menu_data_course_admin_provider
895
     * @param string $url Url of the page we are testing.
896
     * @param string $contextidentifier id or contextid or something similar.
897
     * @param bool $expected The expected return. True to return the overflow menu otherwise false for nothing.
898
     */
899
    public function test_get_overflow_menu_data_course_admin(string $url, string $contextidentifier, bool $expected): void {
900
        global $PAGE;
901
        $this->resetAfterTest();
902
        $this->setAdminUser();
903
 
904
        $pagecourse = $this->getDataGenerator()->create_course();
905
        $contextrecord = \context_course::instance($pagecourse->id, MUST_EXIST);
906
 
907
        $id = ($contextidentifier == 'contextid') ? $contextrecord->id : $pagecourse->id;
908
 
909
        $pageurl = new \moodle_url($url, [$contextidentifier => $id]);
910
        $PAGE->set_url($pageurl);
911
        navigation_node::override_active_url($pageurl);
912
        $PAGE->set_context($contextrecord);
913
 
914
        $node = new secondary($PAGE);
915
        $node->initialise();
916
        $result = $node->get_overflow_menu_data();
917
        if ($expected) {
918
            $this->assertInstanceOf('url_select', $result);
919
            $this->assertTrue($pageurl->compare($result->selected));
920
        } else {
921
            $this->assertNull($result);
922
        }
923
    }
924
 
925
    /**
926
     * Data provider for the other half of the method thing
927
     *
928
     * @return array Provider information.
929
     */
930
    public function get_overflow_menu_data_course_admin_provider(): array {
931
        return [
932
            "Backup page returns overflow" => [
933
                '/backup/backup.php',
934
                'id',
935
                false,
936
            ],
937
            "Restore course page returns overflow" => [
938
                '/backup/restorefile.php',
939
                'contextid',
940
                false,
941
            ],
942
            "Import course page returns overflow" => [
943
                '/backup/import.php',
944
                'id',
945
                false,
946
            ],
947
            "Course copy page returns overflow" => [
948
                '/backup/copy.php',
949
                'id',
950
                false,
951
            ],
952
            "Course reset page returns overflow" => [
953
                '/course/reset.php',
954
                'id',
955
                false,
956
            ],
957
            // The following pages should not return the overflow menu.
958
            "Course page returns nothing" => [
959
                '/course/view.php',
960
                'id',
961
                false
962
            ],
963
            "Question bank should return nothing" => [
964
                '/question/edit.php',
965
                'courseid',
966
                false
967
            ],
968
            "Reports should return nothing" => [
969
                '/report/log/index.php',
970
                'id',
971
                false
972
            ],
973
            "Participants page should return nothing" => [
974
                '/user/index.php',
975
                'id',
976
                false
977
            ]
978
        ];
979
    }
980
}