0e41c873c610b8ff0eb573238d416d77adadf883
[moodle.git] / lib / tests / tablelib_test.php
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/>.
17 /**
18  * Test tablelib.
19  *
20  * @package    core
21  * @category   phpunit
22  * @copyright  2013 Damyon Wiese <damyon@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
29 require_once($CFG->libdir . '/tablelib.php');
30 require_once($CFG->libdir . '/tests/fixtures/testable_flexible_table.php');
32 /**
33  * Test some of tablelib.
34  *
35  * @package    core
36  * @category   phpunit
37  * @copyright  2013 Damyon Wiese <damyon@moodle.com>
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 class core_tablelib_testcase extends advanced_testcase {
41     protected function generate_columns($cols) {
42         $columns = array();
43         foreach (range(0, $cols - 1) as $j) {
44             array_push($columns, 'column' . $j);
45         }
46         return $columns;
47     }
49     protected function generate_headers($cols) {
50         $columns = array();
51         foreach (range(0, $cols - 1) as $j) {
52             array_push($columns, 'Column ' . $j);
53         }
54         return $columns;
55     }
57     protected function generate_data($rows, $cols) {
58         $data = array();
60         foreach (range(0, $rows - 1) as $i) {
61             $row = array();
62             foreach (range(0, $cols - 1) as $j) {
63                 $val =  'row ' . $i . ' col ' . $j;
64                 $row['column' . $j] = $val;
65             }
66             array_push($data, $row);
67         }
68         return $data;
69     }
71     /**
72      * Create a table with properties as passed in params, add data and output html.
73      *
74      * @param string[] $columns
75      * @param string[] $headers
76      * @param bool     $sortable
77      * @param bool     $collapsible
78      * @param string[] $suppress
79      * @param string[] $nosorting
80      * @param (array|object)[] $data
81      * @param int      $pagesize
82      */
83     protected function run_table_test($columns, $headers, $sortable, $collapsible, $suppress, $nosorting, $data, $pagesize) {
84         $table = $this->create_and_setup_table($columns, $headers, $sortable, $collapsible, $suppress, $nosorting);
85         $table->pagesize($pagesize, count($data));
86         foreach ($data as $row) {
87             $table->add_data_keyed($row);
88         }
89         $table->finish_output();
90     }
92     /**
93      * Create a table with properties as passed in params.
94      *
95      * @param string[] $columns
96      * @param string[] $headers
97      * @param bool $sortable
98      * @param bool $collapsible
99      * @param string[] $suppress
100      * @param string[] $nosorting
101      * @return flexible_table
102      */
103     protected function create_and_setup_table($columns, $headers, $sortable, $collapsible, $suppress, $nosorting) {
104         $table = new flexible_table('tablelib_test');
106         $table->define_columns($columns);
107         $table->define_headers($headers);
108         $table->define_baseurl('/invalid.php');
110         $table->sortable($sortable);
111         $table->collapsible($collapsible);
112         foreach ($suppress as $column) {
113             $table->column_suppress($column);
114         }
116         foreach ($nosorting as $column) {
117             $table->no_sorting($column);
118         }
120         $table->setup();
121         return $table;
122     }
124     public function test_empty_table() {
125         $this->expectOutputRegex('/' . get_string('nothingtodisplay') . '/');
126         $this->run_table_test(
127             array('column1', 'column2'),       // Columns.
128             array('Column 1', 'Column 2'),     // Headers.
129             true,                              // Sortable.
130             false,                             // Collapsible.
131             array(),                           // Suppress columns.
132             array(),                           // No sorting.
133             array(),                           // Data.
134             10                                 // Page size.
135         );
136     }
138     public function test_has_next_pagination() {
140         $data = $this->generate_data(11, 2);
141         $columns = $this->generate_columns(2);
142         $headers = $this->generate_headers(2);
144         // Search for pagination controls containing 'page-link"\saria-label="Next"'.
145         $this->expectOutputRegex('/page-link"\saria-label="Next"/');
147         $this->run_table_test(
148             $columns,
149             $headers,
150             true,
151             false,
152             array(),
153             array(),
154             $data,
155             10
156         );
157     }
159     public function test_has_hide() {
161         $data = $this->generate_data(11, 2);
162         $columns = $this->generate_columns(2);
163         $headers = $this->generate_headers(2);
165         // Search for 'hide' links in the column headers.
166         $this->expectOutputRegex('/' . get_string('hide') . '/');
168         $this->run_table_test(
169             $columns,
170             $headers,
171             true,
172             true,
173             array(),
174             array(),
175             $data,
176             10
177         );
178     }
180     public function test_has_not_hide() {
182         $data = $this->generate_data(11, 2);
183         $columns = $this->generate_columns(2);
184         $headers = $this->generate_headers(2);
186         // Make sure there are no 'hide' links in the headers.
188         ob_start();
189         $this->run_table_test(
190             $columns,
191             $headers,
192             true,
193             false,
194             array(),
195             array(),
196             $data,
197             10
198         );
199         $output = ob_get_contents();
200         ob_end_clean();
201         $this->assertNotContains(get_string('hide'), $output);
202     }
204     public function test_has_sort() {
206         $data = $this->generate_data(11, 2);
207         $columns = $this->generate_columns(2);
208         $headers = $this->generate_headers(2);
210         // Search for pagination controls containing '1.*2</a>.*Next</a>'.
211         $this->expectOutputRegex('/' . get_string('sortby') . '/');
213         $this->run_table_test(
214             $columns,
215             $headers,
216             true,
217             false,
218             array(),
219             array(),
220             $data,
221             10
222         );
223     }
225     public function test_has_not_sort() {
227         $data = $this->generate_data(11, 2);
228         $columns = $this->generate_columns(2);
229         $headers = $this->generate_headers(2);
231         // Make sure there are no 'Sort by' links in the headers.
233         ob_start();
234         $this->run_table_test(
235             $columns,
236             $headers,
237             false,
238             false,
239             array(),
240             array(),
241             $data,
242             10
243         );
244         $output = ob_get_contents();
245         ob_end_clean();
246         $this->assertNotContains(get_string('sortby'), $output);
247     }
249     public function test_has_not_next_pagination() {
251         $data = $this->generate_data(10, 2);
252         $columns = $this->generate_columns(2);
253         $headers = $this->generate_headers(2);
255         // Make sure there are no 'Next' links in the pagination.
257         ob_start();
258         $this->run_table_test(
259             $columns,
260             $headers,
261             true,
262             false,
263             array(),
264             array(),
265             $data,
266             10
267         );
269         $output = ob_get_contents();
270         ob_end_clean();
271         $this->assertNotContains(get_string('next'), $output);
272     }
274     public function test_1_col() {
276         $data = $this->generate_data(100, 1);
277         $columns = $this->generate_columns(1);
278         $headers = $this->generate_headers(1);
280         $this->expectOutputRegex('/row 0 col 0/');
282         $this->run_table_test(
283             $columns,
284             $headers,
285             true,
286             false,
287             array(),
288             array(),
289             $data,
290             10
291         );
292     }
294     public function test_empty_rows() {
296         $data = $this->generate_data(1, 5);
297         $columns = $this->generate_columns(5);
298         $headers = $this->generate_headers(5);
300         // Test that we have at least 5 columns generated for each empty row.
301         $this->expectOutputRegex('/emptyrow.*r9_c4/');
303         $this->run_table_test(
304             $columns,
305             $headers,
306             true,
307             false,
308             array(),
309             array(),
310             $data,
311             10
312         );
313     }
315     public function test_5_cols() {
317         $data = $this->generate_data(100, 5);
318         $columns = $this->generate_columns(5);
319         $headers = $this->generate_headers(5);
321         $this->expectOutputRegex('/row 0 col 0/');
323         $this->run_table_test(
324             $columns,
325             $headers,
326             true,
327             false,
328             array(),
329             array(),
330             $data,
331             10
332         );
333     }
335     public function test_50_cols() {
337         $data = $this->generate_data(100, 50);
338         $columns = $this->generate_columns(50);
339         $headers = $this->generate_headers(50);
341         $this->expectOutputRegex('/row 0 col 0/');
343         $this->run_table_test(
344             $columns,
345             $headers,
346             true,
347             false,
348             array(),
349             array(),
350             $data,
351             10
352         );
353     }
355     /**
356      * Data provider for test_fullname_column
357      *
358      * @return array
359      */
360     public function fullname_column_provider() {
361         return [
362             ['language'],
363             ['alternatename lastname'],
364             ['firstname lastnamephonetic'],
365         ];
366     }
368     /**
369      * Test fullname column observes configured alternate fullname format configuration
370      *
371      * @param string $format
372      * @return void
373      *
374      * @dataProvider fullname_column_provider
375      */
376     public function test_fullname_column(string $format) {
377         $this->resetAfterTest();
378         $this->setAdminUser();
380         set_config('alternativefullnameformat', $format);
382         $user = $this->getDataGenerator()->create_user();
384         $table = $this->create_and_setup_table(['fullname'], [], true, false, [], []);
385         $this->assertContains(fullname($user, true), $table->format_row($user)['fullname']);
386     }
388     /**
389      * Test fullname column ignores fullname format configuration for a user with viewfullnames capability prohibited
390      *
391      * @param string $format
392      * @return void
393      *
394      * @dataProvider fullname_column_provider
395      */
396     public function test_fullname_column_prohibit_viewfullnames(string $format) {
397         global $DB, $CFG;
399         $this->resetAfterTest();
401         set_config('alternativefullnameformat', $format);
403         $currentuser = $this->getDataGenerator()->create_user();
404         $this->setUser($currentuser);
406         // Prohibit the viewfullnames from the default user role.
407         $userrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid]);
408         role_change_permission($userrole->id, context_system::instance(), 'moodle/site:viewfullnames', CAP_PROHIBIT);
410         $user = $this->getDataGenerator()->create_user();
412         $table = $this->create_and_setup_table(['fullname'], [], true, false, [], []);
413         $this->assertContains(fullname($user, false), $table->format_row($user)['fullname']);
414     }
416     public function test_get_row_html() {
417         $data = $this->generate_data(1, 5);
418         $columns = $this->generate_columns(5);
419         $headers = $this->generate_headers(5);
420         $data = array_keys(array_flip($data[0]));
422         $table = new flexible_table('tablelib_test');
423         $table->define_columns($columns);
424         $table->define_headers($headers);
425         $table->define_baseurl('/invalid.php');
427         $row = $table->get_row_html($data);
428         $this->assertRegExp('/row 0 col 0/', $row);
429         $this->assertRegExp('/<tr class=""/', $row);
430         $this->assertRegExp('/<td class="cell c0"/', $row);
431     }
433     public function test_persistent_table() {
434         global $SESSION;
436         $data = $this->generate_data(5, 5);
437         $columns = $this->generate_columns(5);
438         $headers = $this->generate_headers(5);
440         // Testing without persistence first to verify that the results are different.
441         $table1 = new flexible_table('tablelib_test');
442         $table1->define_columns($columns);
443         $table1->define_headers($headers);
444         $table1->define_baseurl('/invalid.php');
446         $table1->sortable(true);
447         $table1->collapsible(true);
449         $table1->is_persistent(false);
450         $_GET['thide'] = 'column0';
451         $_GET['tsort'] = 'column1';
452         $_GET['tifirst'] = 'A';
453         $_GET['tilast'] = 'Z';
455         foreach ($data as $row) {
456             $table1->add_data_keyed($row);
457         }
458         $table1->setup();
460         // Clear session data between each new table.
461         unset($SESSION->flextable);
463         $table2 = new flexible_table('tablelib_test');
464         $table2->define_columns($columns);
465         $table2->define_headers($headers);
466         $table2->define_baseurl('/invalid.php');
468         $table2->sortable(true);
469         $table2->collapsible(true);
471         $table2->is_persistent(false);
472         unset($_GET);
474         foreach ($data as $row) {
475             $table2->add_data_keyed($row);
476         }
477         $table2->setup();
479         $this->assertNotEquals($table1, $table2);
481         unset($SESSION->flextable);
483         // Now testing with persistence to check that the tables are the same.
484         $table3 = new flexible_table('tablelib_test');
485         $table3->define_columns($columns);
486         $table3->define_headers($headers);
487         $table3->define_baseurl('/invalid.php');
489         $table3->sortable(true);
490         $table3->collapsible(true);
492         $table3->is_persistent(true);
493         $_GET['thide'] = 'column0';
494         $_GET['tsort'] = 'column1';
495         $_GET['tifirst'] = 'A';
496         $_GET['tilast'] = 'Z';
498         foreach ($data as $row) {
499             $table3->add_data_keyed($row);
500         }
501         $table3->setup();
503         unset($SESSION->flextable);
505         $table4 = new flexible_table('tablelib_test');
506         $table4->define_columns($columns);
507         $table4->define_headers($headers);
508         $table4->define_baseurl('/invalid.php');
510         $table4->sortable(true);
511         $table4->collapsible(true);
513         $table4->is_persistent(true);
514         unset($_GET);
516         foreach ($data as $row) {
517             $table4->add_data_keyed($row);
518         }
519         $table4->setup();
521         $this->assertEquals($table3, $table4);
523         unset($SESSION->flextable);
525         // Finally, another test with no persistence, but without clearing the session data.
526         $table5 = new flexible_table('tablelib_test');
527         $table5->define_columns($columns);
528         $table5->define_headers($headers);
529         $table5->define_baseurl('/invalid.php');
531         $table5->sortable(true);
532         $table5->collapsible(true);
534         $table5->is_persistent(true);
535         $_GET['thide'] = 'column0';
536         $_GET['tsort'] = 'column1';
537         $_GET['tifirst'] = 'A';
538         $_GET['tilast'] = 'Z';
540         foreach ($data as $row) {
541             $table5->add_data_keyed($row);
542         }
543         $table5->setup();
545         $table6 = new flexible_table('tablelib_test');
546         $table6->define_columns($columns);
547         $table6->define_headers($headers);
548         $table6->define_baseurl('/invalid.php');
550         $table6->sortable(true);
551         $table6->collapsible(true);
553         $table6->is_persistent(true);
554         unset($_GET);
556         foreach ($data as $row) {
557             $table6->add_data_keyed($row);
558         }
559         $table6->setup();
561         $this->assertEquals($table5, $table6);
562     }
564     /**
565      * Helper method for preparing tables instances in {@link self::test_can_be_reset()}.
566      *
567      * @param string $tableid
568      * @return testable_flexible_table
569      */
570     protected function prepare_table_for_reset_test($tableid) {
571         global $SESSION;
573         unset($SESSION->flextable[$tableid]);
575         $data = $this->generate_data(25, 3);
576         $columns = array('column0', 'column1', 'column2');
577         $headers = $this->generate_headers(3);
579         $table = new testable_flexible_table($tableid);
580         $table->define_baseurl('/invalid.php');
581         $table->define_columns($columns);
582         $table->define_headers($headers);
583         $table->collapsible(true);
584         $table->is_persistent(false);
586         return $table;
587     }
589     public function test_can_be_reset() {
590         // Table in its default state (as if seen for the first time), nothing to reset.
591         $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
592         $table->setup();
593         $this->assertFalse($table->can_be_reset());
595         // Table in its default state with default sorting defined, nothing to reset.
596         $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
597         $table->sortable(true, 'column1', SORT_DESC);
598         $table->setup();
599         $this->assertFalse($table->can_be_reset());
601         // Table explicitly sorted by the default column & direction, nothing to reset.
602         $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
603         $table->sortable(true, 'column1', SORT_DESC);
604         $_GET['tsort'] = 'column1';
605         $_GET['tdir'] = SORT_DESC;
606         $table->setup();
607         unset($_GET['tsort']);
608         unset($_GET['tdir']);
609         $this->assertFalse($table->can_be_reset());
611         // Table explicitly sorted twice by the default column & direction, nothing to reset.
612         $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
613         $table->sortable(true, 'column1', SORT_DESC);
614         $_GET['tsort'] = 'column1';
615         $_GET['tdir'] = SORT_DESC;
616         $table->setup();
617         $table->setup(); // Set up again to simulate the second page request.
618         unset($_GET['tsort']);
619         unset($_GET['tdir']);
620         $this->assertFalse($table->can_be_reset());
622         // Table sorted by other than default column, can be reset.
623         $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
624         $table->sortable(true, 'column1', SORT_DESC);
625         $_GET['tsort'] = 'column2';
626         $table->setup();
627         unset($_GET['tsort']);
628         $this->assertTrue($table->can_be_reset());
630         // Table sorted by other than default direction, can be reset.
631         $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
632         $table->sortable(true, 'column1', SORT_DESC);
633         $_GET['tsort'] = 'column1';
634         $_GET['tdir'] = SORT_ASC;
635         $table->setup();
636         unset($_GET['tsort']);
637         unset($_GET['tdir']);
638         $this->assertTrue($table->can_be_reset());
640         // Table sorted by the default column after another sorting previously selected.
641         // This leads to different ORDER BY than just having a single sort defined, can be reset.
642         $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
643         $table->sortable(true, 'column1', SORT_DESC);
644         $_GET['tsort'] = 'column0';
645         $table->setup();
646         $_GET['tsort'] = 'column1';
647         $table->setup();
648         unset($_GET['tsort']);
649         $this->assertTrue($table->can_be_reset());
651         // Table having some column collapsed, can be reset.
652         $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
653         $_GET['thide'] = 'column2';
654         $table->setup();
655         unset($_GET['thide']);
656         $this->assertTrue($table->can_be_reset());
658         // Table having some column explicitly expanded, nothing to reset.
659         $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
660         $_GET['tshow'] = 'column2';
661         $table->setup();
662         unset($_GET['tshow']);
663         $this->assertFalse($table->can_be_reset());
665         // Table after expanding a collapsed column, nothing to reset.
666         $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
667         $_GET['thide'] = 'column0';
668         $table->setup();
669         $_GET['tshow'] = 'column0';
670         $table->setup();
671         unset($_GET['thide']);
672         unset($_GET['tshow']);
673         $this->assertFalse($table->can_be_reset());
675         // Table with some name filtering enabled, can be reset.
676         $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
677         $_GET['tifirst'] = 'A';
678         $table->setup();
679         unset($_GET['tifirst']);
680         $this->assertTrue($table->can_be_reset());
681     }
683     /**
684      * Test export in CSV format
685      */
686     public function test_table_export() {
687         $table = new flexible_table('tablelib_test_export');
688         $table->define_baseurl('/invalid.php');
689         $table->define_columns(['c1', 'c2', 'c3']);
690         $table->define_headers(['Col1', 'Col2', 'Col3']);
692         ob_start();
693         $table->is_downloadable(true);
694         $table->is_downloading('csv');
696         $table->setup();
697         $table->add_data(['column0' => 'a', 'column1' => 'b', 'column2' => 'c']);
698         $output = ob_get_contents();
699         ob_end_clean();
701         $this->assertEquals("Col1,Col2,Col3\na,b,c\n", substr($output, 3));
702     }
704     /**
705      * Test the initials functionality.
706      *
707      * @dataProvider initials_provider
708      * @param string|null $getvalue
709      * @param string|null $setvalue
710      * @param string|null $finalvalue
711      */
712     public function test_initials_first_set(?string $getvalue, ?string $setvalue, ?string $finalvalue): void {
713         global $_GET;
715         $this->resetAfterTest(true);
717         $table = new flexible_table('tablelib_test');
719         $user = $this->getDataGenerator()->create_user();
721         $table->define_columns(['fullname']);
722         $table->define_headers(['Fullname']);
723         $table->define_baseurl('/invalid.php');
724         $table->initialbars(true);
726         if ($getvalue !== null) {
727             $_GET['tifirst'] = $getvalue;
728         }
730         if ($setvalue !== null) {
731             $table->set_first_initial($setvalue);
732         }
734         $table->setup();
736         $this->assertEquals($finalvalue, $table->get_initial_first());
737     }
739     /**
740      * Test the initials functionality.
741      *
742      * @dataProvider initials_provider
743      * @param string|null $getvalue
744      * @param string|null $setvalue
745      * @param string|null $finalvalue
746      */
747     public function test_initials_last_set(?string $getvalue, ?string $setvalue, ?string $finalvalue): void {
748         global $_GET;
750         $this->resetAfterTest(true);
752         $table = new flexible_table('tablelib_test');
754         $user = $this->getDataGenerator()->create_user();
756         $table->define_columns(['fullname']);
757         $table->define_headers(['Fullname']);
758         $table->define_baseurl('/invalid.php');
759         $table->initialbars(true);
761         if ($getvalue !== null) {
762             $_GET['tilast'] = $getvalue;
763         }
765         if ($setvalue !== null) {
766             $table->set_last_initial($setvalue);
767         }
769         $table->setup();
771         $this->assertEquals($finalvalue, $table->get_initial_last());
772     }
774     /**
775      * Data for testing initials providers.
776      *
777      * @return array
778      */
779     public function initials_provider(): array {
780         return [
781             [null, null, null],
782             ['A', null, 'A'],
783             ['Z', null, 'Z'],
784             [null, 'A', 'A'],
785             [null, 'Z', 'Z'],
786             ['A', 'Z', 'Z'],
787             ['Z', 'A', 'A'],
788         ];
789     }