2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
20 use testable_flexible_table
;
22 defined('MOODLE_INTERNAL') ||
die();
25 require_once($CFG->libdir
. '/tablelib.php');
26 require_once($CFG->libdir
. '/tests/fixtures/testable_flexible_table.php');
29 * Test some of tablelib.
33 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 class tablelib_test
extends \advanced_testcase
{
37 protected function generate_columns($cols) {
39 foreach (range(0, $cols - 1) as $j) {
40 array_push($columns, 'column' . $j);
45 protected function generate_headers($cols) {
47 foreach (range(0, $cols - 1) as $j) {
48 array_push($columns, 'Column ' . $j);
53 protected function generate_data($rows, $cols) {
56 foreach (range(0, $rows - 1) as $i) {
58 foreach (range(0, $cols - 1) as $j) {
59 $val = 'row ' . $i . ' col ' . $j;
60 $row['column' . $j] = $val;
62 array_push($data, $row);
68 * Create a table with properties as passed in params, add data and output html.
70 * @param string[] $columns The columns of the table.
71 * @param string[] $headers The header of the table.
72 * @param bool $sortable Sorting of the table.
73 * @param bool $collapsible Is table collapsible.
74 * @param string[] $suppress Suppress columns.
75 * @param string[] $nosorting No sorting.
76 * @param (array|object)[] $data The data of the table.
77 * @param int $pagesize Page size of the table
78 * @param string $caption Caption of the table.
79 * @param array $captionattribute The attribute of the caption.
81 protected function run_table_test($columns, $headers, $sortable, $collapsible, $suppress, $nosorting, $data,
82 $pagesize, $caption = '', $captionattribute = []) {
83 $table = $this->create_and_setup_table($columns, $headers, $sortable, $collapsible, $suppress, $nosorting,
84 $caption, $captionattribute);
85 $table->pagesize($pagesize, count($data));
86 foreach ($data as $row) {
87 $table->add_data_keyed($row);
89 $table->finish_output();
93 * Create a table with properties as passed in params.
95 * @param string[] $columns The columns of the table.
96 * @param string[] $headers The header of the table.
97 * @param bool $sortable Sorting of the table.
98 * @param bool $collapsible Is table collapsible.
99 * @param string[] $suppress Suppress columns.
100 * @param string[] $nosorting No sorting.
101 * @param string $caption Caption of the table.
102 * @param array $captionattribute The attribute of the caption.
103 * @return flexible_table
105 protected function create_and_setup_table($columns, $headers, $sortable, $collapsible, $suppress, $nosorting,
106 $caption = '', $captionattribute = '') {
107 $table = new flexible_table('tablelib_test');
109 $table->define_columns($columns);
110 $table->define_headers($headers);
111 $table->define_baseurl('/invalid.php');
113 $table->sortable($sortable);
114 $table->collapsible($collapsible);
115 foreach ($suppress as $column) {
116 $table->column_suppress($column);
119 foreach ($nosorting as $column) {
120 $table->no_sorting($column);
123 $table->set_caption($caption, $captionattribute);
130 public function test_empty_table() {
131 $this->expectOutputRegex('/' . get_string('nothingtodisplay') . '/');
132 $this->run_table_test(
133 array('column1', 'column2'), // Columns.
134 array('Column 1', 'Column 2'), // Headers.
136 false, // Collapsible.
137 array(), // Suppress columns.
138 array(), // No sorting.
144 public function test_has_next_pagination() {
146 $data = $this->generate_data(11, 2);
147 $columns = $this->generate_columns(2);
148 $headers = $this->generate_headers(2);
150 // Search for pagination controls containing 'page-link"\saria-label="Next"'.
151 $this->expectOutputRegex('/Next page/');
153 $this->run_table_test(
165 public function test_has_hide() {
167 $data = $this->generate_data(11, 2);
168 $columns = $this->generate_columns(2);
169 $headers = $this->generate_headers(2);
171 // Search for 'hide' links in the column headers.
172 $this->expectOutputRegex('/' . get_string('hide') . '/');
174 $this->run_table_test(
186 public function test_has_not_hide() {
188 $data = $this->generate_data(11, 2);
189 $columns = $this->generate_columns(2);
190 $headers = $this->generate_headers(2);
192 // Make sure there are no 'hide' links in the headers.
195 $this->run_table_test(
205 $output = ob_get_contents();
207 $this->assertStringNotContainsString(get_string('hide'), $output);
210 public function test_has_sort() {
212 $data = $this->generate_data(11, 2);
213 $columns = $this->generate_columns(2);
214 $headers = $this->generate_headers(2);
216 // Search for pagination controls containing '1.*2</a>.*Next</a>'.
217 $this->expectOutputRegex('/' . get_string('sortby') . '/');
219 $this->run_table_test(
231 public function test_has_not_sort() {
233 $data = $this->generate_data(11, 2);
234 $columns = $this->generate_columns(2);
235 $headers = $this->generate_headers(2);
237 // Make sure there are no 'Sort by' links in the headers.
240 $this->run_table_test(
250 $output = ob_get_contents();
252 $this->assertStringNotContainsString(get_string('sortby'), $output);
255 public function test_has_not_next_pagination() {
257 $data = $this->generate_data(10, 2);
258 $columns = $this->generate_columns(2);
259 $headers = $this->generate_headers(2);
261 // Make sure there are no 'Next' links in the pagination.
264 $this->run_table_test(
275 $output = ob_get_contents();
277 $this->assertStringNotContainsString(get_string('next'), $output);
280 public function test_1_col() {
282 $data = $this->generate_data(100, 1);
283 $columns = $this->generate_columns(1);
284 $headers = $this->generate_headers(1);
286 $this->expectOutputRegex('/row 0 col 0/');
288 $this->run_table_test(
300 public function test_empty_rows() {
302 $data = $this->generate_data(1, 5);
303 $columns = $this->generate_columns(5);
304 $headers = $this->generate_headers(5);
306 // Test that we have at least 5 columns generated for each empty row.
307 $this->expectOutputRegex('/emptyrow.*r9_c4/');
309 $this->run_table_test(
321 public function test_5_cols() {
323 $data = $this->generate_data(100, 5);
324 $columns = $this->generate_columns(5);
325 $headers = $this->generate_headers(5);
327 $this->expectOutputRegex('/row 0 col 0/');
329 $this->run_table_test(
341 public function test_50_cols() {
343 $data = $this->generate_data(100, 50);
344 $columns = $this->generate_columns(50);
345 $headers = $this->generate_headers(50);
347 $this->expectOutputRegex('/row 0 col 0/');
349 $this->run_table_test(
362 * Data provider for test_fullname_column
366 public function fullname_column_provider() {
369 ['alternatename lastname'],
370 ['firstname lastnamephonetic'],
375 * Test fullname column observes configured alternate fullname format configuration
377 * @param string $format
380 * @dataProvider fullname_column_provider
382 public function test_fullname_column(string $format) {
383 $this->resetAfterTest();
384 $this->setAdminUser();
386 set_config('alternativefullnameformat', $format);
388 $user = $this->getDataGenerator()->create_user();
390 $table = $this->create_and_setup_table(['fullname'], [], true, false, [], []);
391 $this->assertStringContainsString(fullname($user, true), $table->format_row($user)['fullname']);
395 * Test fullname column ignores fullname format configuration for a user with viewfullnames capability prohibited
397 * @param string $format
400 * @dataProvider fullname_column_provider
402 public function test_fullname_column_prohibit_viewfullnames(string $format) {
405 $this->resetAfterTest();
407 set_config('alternativefullnameformat', $format);
409 $currentuser = $this->getDataGenerator()->create_user();
410 $this->setUser($currentuser);
412 // Prohibit the viewfullnames from the default user role.
413 $userrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid
]);
414 role_change_permission($userrole->id
, \context_system
::instance(), 'moodle/site:viewfullnames', CAP_PROHIBIT
);
416 $user = $this->getDataGenerator()->create_user();
418 $table = $this->create_and_setup_table(['fullname'], [], true, false, [], []);
419 $this->assertStringContainsString(fullname($user, false), $table->format_row($user)['fullname']);
422 public function test_get_row_html() {
423 $data = $this->generate_data(1, 5);
424 $columns = $this->generate_columns(5);
425 $headers = $this->generate_headers(5);
426 $data = array_keys(array_flip($data[0]));
428 $table = new flexible_table('tablelib_test');
429 $table->define_columns($columns);
430 $table->define_headers($headers);
431 $table->define_baseurl('/invalid.php');
433 $row = $table->get_row_html($data);
434 $this->assertMatchesRegularExpression('/row 0 col 0/', $row);
435 $this->assertMatchesRegularExpression('/<tr class=""/', $row);
436 $this->assertMatchesRegularExpression('/<td class="cell c0"/', $row);
439 public function test_persistent_table() {
442 $data = $this->generate_data(5, 5);
443 $columns = $this->generate_columns(5);
444 $headers = $this->generate_headers(5);
446 // Testing without persistence first to verify that the results are different.
447 $table1 = new flexible_table('tablelib_test');
448 $table1->define_columns($columns);
449 $table1->define_headers($headers);
450 $table1->define_baseurl('/invalid.php');
452 $table1->sortable(true);
453 $table1->collapsible(true);
455 $table1->is_persistent(false);
456 $_GET['thide'] = 'column0';
457 $_GET['tsort'] = 'column1';
458 $_GET['tifirst'] = 'A';
459 $_GET['tilast'] = 'Z';
461 foreach ($data as $row) {
462 $table1->add_data_keyed($row);
466 // Clear session data between each new table.
467 unset($SESSION->flextable
);
469 $table2 = new flexible_table('tablelib_test');
470 $table2->define_columns($columns);
471 $table2->define_headers($headers);
472 $table2->define_baseurl('/invalid.php');
474 $table2->sortable(true);
475 $table2->collapsible(true);
477 $table2->is_persistent(false);
480 foreach ($data as $row) {
481 $table2->add_data_keyed($row);
485 $this->assertNotEquals($table1, $table2);
487 unset($SESSION->flextable
);
489 // Now testing with persistence to check that the tables are the same.
490 $table3 = new flexible_table('tablelib_test');
491 $table3->define_columns($columns);
492 $table3->define_headers($headers);
493 $table3->define_baseurl('/invalid.php');
495 $table3->sortable(true);
496 $table3->collapsible(true);
498 $table3->is_persistent(true);
499 $_GET['thide'] = 'column0';
500 $_GET['tsort'] = 'column1';
501 $_GET['tifirst'] = 'A';
502 $_GET['tilast'] = 'Z';
504 foreach ($data as $row) {
505 $table3->add_data_keyed($row);
509 unset($SESSION->flextable
);
511 $table4 = new flexible_table('tablelib_test');
512 $table4->define_columns($columns);
513 $table4->define_headers($headers);
514 $table4->define_baseurl('/invalid.php');
516 $table4->sortable(true);
517 $table4->collapsible(true);
519 $table4->is_persistent(true);
522 foreach ($data as $row) {
523 $table4->add_data_keyed($row);
527 $this->assertEquals($table3, $table4);
529 unset($SESSION->flextable
);
531 // Finally, another test with no persistence, but without clearing the session data.
532 $table5 = new flexible_table('tablelib_test');
533 $table5->define_columns($columns);
534 $table5->define_headers($headers);
535 $table5->define_baseurl('/invalid.php');
537 $table5->sortable(true);
538 $table5->collapsible(true);
540 $table5->is_persistent(true);
541 $_GET['thide'] = 'column0';
542 $_GET['tsort'] = 'column1';
543 $_GET['tifirst'] = 'A';
544 $_GET['tilast'] = 'Z';
546 foreach ($data as $row) {
547 $table5->add_data_keyed($row);
551 $table6 = new flexible_table('tablelib_test');
552 $table6->define_columns($columns);
553 $table6->define_headers($headers);
554 $table6->define_baseurl('/invalid.php');
556 $table6->sortable(true);
557 $table6->collapsible(true);
559 $table6->is_persistent(true);
562 foreach ($data as $row) {
563 $table6->add_data_keyed($row);
567 $this->assertEquals($table5, $table6);
571 * Helper method for preparing tables instances in {@link self::test_can_be_reset()}.
573 * @param string $tableid
574 * @return testable_flexible_table
576 protected function prepare_table_for_reset_test($tableid) {
579 unset($SESSION->flextable
[$tableid]);
581 $data = $this->generate_data(25, 3);
582 $columns = array('column0', 'column1', 'column2');
583 $headers = $this->generate_headers(3);
585 $table = new testable_flexible_table($tableid);
586 $table->define_baseurl('/invalid.php');
587 $table->define_columns($columns);
588 $table->define_headers($headers);
589 $table->collapsible(true);
590 $table->is_persistent(false);
595 public function test_can_be_reset() {
596 // Table in its default state (as if seen for the first time), nothing to reset.
597 $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
599 $this->assertFalse($table->can_be_reset());
601 // Table in its default state with default sorting defined, nothing to reset.
602 $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
603 $table->sortable(true, 'column1', SORT_DESC
);
605 $this->assertFalse($table->can_be_reset());
607 // Table explicitly sorted by the default column & direction, nothing to reset.
608 $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
609 $table->sortable(true, 'column1', SORT_DESC
);
610 $_GET['tsort'] = 'column1';
611 $_GET['tdir'] = SORT_DESC
;
613 unset($_GET['tsort']);
614 unset($_GET['tdir']);
615 $this->assertFalse($table->can_be_reset());
617 // Table explicitly sorted twice by the default column & direction, nothing to reset.
618 $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
619 $table->sortable(true, 'column1', SORT_DESC
);
620 $_GET['tsort'] = 'column1';
621 $_GET['tdir'] = SORT_DESC
;
623 $table->setup(); // Set up again to simulate the second page request.
624 unset($_GET['tsort']);
625 unset($_GET['tdir']);
626 $this->assertFalse($table->can_be_reset());
628 // Table sorted by other than default column, can be reset.
629 $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
630 $table->sortable(true, 'column1', SORT_DESC
);
631 $_GET['tsort'] = 'column2';
633 unset($_GET['tsort']);
634 $this->assertTrue($table->can_be_reset());
636 // Table sorted by other than default direction, can be reset.
637 $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
638 $table->sortable(true, 'column1', SORT_DESC
);
639 $_GET['tsort'] = 'column1';
640 $_GET['tdir'] = SORT_ASC
;
642 unset($_GET['tsort']);
643 unset($_GET['tdir']);
644 $this->assertTrue($table->can_be_reset());
646 // Table sorted by the default column after another sorting previously selected.
647 // This leads to different ORDER BY than just having a single sort defined, can be reset.
648 $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
649 $table->sortable(true, 'column1', SORT_DESC
);
650 $_GET['tsort'] = 'column0';
652 $_GET['tsort'] = 'column1';
654 unset($_GET['tsort']);
655 $this->assertTrue($table->can_be_reset());
657 // Table having some column collapsed, can be reset.
658 $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
659 $_GET['thide'] = 'column2';
661 unset($_GET['thide']);
662 $this->assertTrue($table->can_be_reset());
664 // Table having some column explicitly expanded, nothing to reset.
665 $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
666 $_GET['tshow'] = 'column2';
668 unset($_GET['tshow']);
669 $this->assertFalse($table->can_be_reset());
671 // Table after expanding a collapsed column, nothing to reset.
672 $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
673 $_GET['thide'] = 'column0';
675 $_GET['tshow'] = 'column0';
677 unset($_GET['thide']);
678 unset($_GET['tshow']);
679 $this->assertFalse($table->can_be_reset());
681 // Table with some name filtering enabled, can be reset.
682 $table = $this->prepare_table_for_reset_test(uniqid('tablelib_test_'));
683 $_GET['tifirst'] = 'A';
685 unset($_GET['tifirst']);
686 $this->assertTrue($table->can_be_reset());
690 * Test export in CSV format
692 public function test_table_export() {
693 $table = new flexible_table('tablelib_test_export');
694 $table->define_baseurl('/invalid.php');
695 $table->define_columns(['c1', 'c2', 'c3']);
696 $table->define_headers(['Col1', 'Col2', 'Col3']);
699 $table->is_downloadable(true);
700 $table->is_downloading('csv');
703 $table->add_data(['column0' => 'a', 'column1' => 'b', 'column2' => 'c']);
704 $output = ob_get_contents();
707 $this->assertEquals("Col1,Col2,Col3\na,b,c\n", substr($output, 3));
711 * Test the initials functionality.
713 * @dataProvider initials_provider
714 * @param string|null $getvalue
715 * @param string|null $setvalue
716 * @param string|null $finalvalue
718 public function test_initials_first_set(?
string $getvalue, ?
string $setvalue, ?
string $finalvalue): void
{
721 $this->resetAfterTest(true);
723 $table = new flexible_table('tablelib_test');
725 $user = $this->getDataGenerator()->create_user();
727 $table->define_columns(['fullname']);
728 $table->define_headers(['Fullname']);
729 $table->define_baseurl('/invalid.php');
730 $table->initialbars(true);
732 if ($getvalue !== null) {
733 $_GET['tifirst'] = $getvalue;
736 if ($setvalue !== null) {
737 $table->set_first_initial($setvalue);
742 $this->assertEquals($finalvalue, $table->get_initial_first());
746 * Test the initials functionality.
748 * @dataProvider initials_provider
749 * @param string|null $getvalue
750 * @param string|null $setvalue
751 * @param string|null $finalvalue
753 public function test_initials_last_set(?
string $getvalue, ?
string $setvalue, ?
string $finalvalue): void
{
756 $this->resetAfterTest(true);
758 $table = new flexible_table('tablelib_test');
760 $user = $this->getDataGenerator()->create_user();
762 $table->define_columns(['fullname']);
763 $table->define_headers(['Fullname']);
764 $table->define_baseurl('/invalid.php');
765 $table->initialbars(true);
767 if ($getvalue !== null) {
768 $_GET['tilast'] = $getvalue;
771 if ($setvalue !== null) {
772 $table->set_last_initial($setvalue);
777 $this->assertEquals($finalvalue, $table->get_initial_last());
781 * Data for testing initials providers.
785 public function initials_provider(): array {
798 * Data test for set and render caption for table.
800 * @covers ::set_caption_for_table
801 * @covers ::render_caption_for_table
803 public function test_set_and_render_caption_for_table(): void
{
804 $data = $this->generate_data(10, 2);
805 $columns = $this->generate_columns(2);
806 $headers = $this->generate_headers(2);
807 $caption = 'Caption for table';
808 $captionattribute = ['class' => 'inline'];
809 $this->run_table_test(
824 // Caption for table.
826 // Caption attribute.
829 $this->expectOutputRegex('/' . '<caption class="inline">' . $caption . '<\/caption>' . '/');