. **/ require_once dirname(__FILE__) . '/../../include/CWebTest.php'; define('CURRENT_YEAR', date("Y")); /** * @onBefore prepareHostDashboardsData * * @backup hosts */ class testDashboardsHostDashboardPage extends CWebTest { const HOST_NAME = 'Host for Host Dashboards'; const TEMPLATE_NAME = 'Template for '.self::HOST_NAME; const COUNT_MANY = 20; public function prepareHostDashboardsData() { $data = [ 'host_name' => self::HOST_NAME, 'dashboards' => [ [ 'name' => 'Dashboard 1', 'pages' => [ [ 'name' => 'Page 1', 'widgets' => [ [ 'type' => 'svggraph', 'name' => 'Graph widget', 'width' => 6, 'height' => 4, 'fields' => [ [ 'type' => ZBX_WIDGET_FIELD_TYPE_INT32, 'name' => '*', 'value' => 0 ] ] ] ] ], [ 'name' => 'Page 2' ] ] ] ] ]; $this->createHostWithDashboards($data); // Create a Host with many Dashboards and another Host with many Pages. $dashboard_array = []; $page_array = []; for ($i = 1; $i <= self::COUNT_MANY; $i++) { $dashboard_array[] = ['name' => 'Dashboard '.$i]; $page_array[] = ['name' => 'Page '.$i]; } $data_dashboards = [ 'host_name' => 'Many Dashboards', 'dashboards' => $dashboard_array ]; $this->createHostWithDashboards($data_dashboards); $data_pages = [ 'host_name' => 'Many Pages', 'dashboards' => [ [ 'name' => 'Dashboard 1', 'pages' => $page_array ] ] ]; $this->createHostWithDashboards($data_pages); } /** * Check layout. */ public function testDashboardsHostDashboardPage_Layout() { $this->openDashboardsForHost(self::HOST_NAME); $this->page->assertTitle('Dashboards'); $this->page->assertHeader('Host dashboards'); $breadcrumbs = $this->query('class:breadcrumbs')->one(); $this->assertEquals('zabbix.php?action=host.view', $breadcrumbs->query('link:All hosts')->one()->getAttribute('href')); $this->assertEquals(['All hosts', self::HOST_NAME], $breadcrumbs->query('tag:li')->all()->asText()); $host_dashboard_navigation = $this->query('class:host-dashboard-navigation')->one(); $prev_button = $host_dashboard_navigation->query('xpath:.//button[@title="Previous dashboard"]')->one(); $this->assertTrue($prev_button->isDisplayed()); $this->assertFalse($prev_button->isEnabled()); $dashboard_tab = $host_dashboard_navigation->query('xpath:.//span[text()="Dashboard 1"]')->one(); $this->assertEquals('Dashboard 1', $dashboard_tab->getAttribute('title')); // Assert the listed dashboard dropdown. $list_button = $host_dashboard_navigation->query('xpath:.//button[@title="Dashboard list"]')->one(); $this->assertTrue($list_button->isClickable()); $list_button->click(); $popup_menu = $list_button->asPopupButton()->getMenu(); $this->assertEquals(['Dashboard 1'], $popup_menu->getItems()->asText()); $popup_menu->close(); $next_button = $host_dashboard_navigation->query('xpath:.//button[@title="Next dashboard"]')->one(); $this->assertTrue($next_button->isDisplayed()); $this->assertFalse($next_button->isEnabled()); // Check page tabs. $dashboard_navigation = $this->query('class:dashboard-navigation')->one(); $this->assertEquals(['Page 1', 'Page 2'], $dashboard_navigation->query('xpath:.//li[@class="sortable-item"]')->all()->asText()); // Check Slideshow button. foreach (['Stop', 'Start'] as $status) { $this->assertTrue($dashboard_navigation->query('xpath:.//button/span[text()="'.$status.' slideshow"]')->one()->isDisplayed()); $dashboard_navigation->query('xpath:.//button['.CXPathHelper::fromClass('btn-dashboard-toggle-slideshow').']')->one()->click(); } } /** * Open and close the Kiosk mode. */ public function testDashboardsHostDashboardPage_CheckKioskMode() { $this->openDashboardsForHost(self::HOST_NAME); // Test Kiosk mode. $this->query('xpath://button[@title="Kiosk mode"]')->one()->click(); $this->page->waitUntilReady(); // Check that Header and Filter disappeared. $this->query('xpath://h1[@id="page-title-general"]')->waitUntilNotVisible(); $this->assertFalse(CFilterElement::find()->one()->isVisible()); $this->assertFalse($this->query('class:host-dashboard-navigation')->exists()); $this->assertTrue(CDashboardElement::find()->one()->getWidgets()->first()->isVisible()); // Check that Breadcrumbs are still visible. $breadcrumbs = $this->query('xpath://ul[@class="breadcrumbs"]//span')->all(); $this->assertEquals(['All hosts', self::HOST_NAME], $breadcrumbs->asText()); foreach ($breadcrumbs as $breadcrumb) { $this->assertTrue($breadcrumb->isVisible()); } // Check Dashboard page controls. foreach (['Previous page', 'Stop slideshow', 'Next page'] as $button) { $this->assertTrue($this->query('xpath://button[@title="'.$button.'"]')->exists()); } // Check Slideshow button. $dashboard_controls = $this->query('class:dashboard-kioskmode-controls')->one(); foreach (['Stop', 'Start'] as $status) { $this->assertTrue($dashboard_controls->query('xpath:.//button[@title="'.$status.' slideshow"]')->one()->isDisplayed()); $dashboard_controls->query('xpath:.//button['. CXPathHelper::fromClass('btn-dashboard-kioskmode-toggle-slideshow').']')->one()->click(); } $this->query('xpath://button[@title="Normal view"]')->waitUntilPresent()->one()->hoverMouse()->click(); $this->page->waitUntilReady(); // Check that Header and Filter are visible again. $this->query('xpath://h1[@id="page-title-general"]')->waitUntilVisible(); foreach (['xpath://div['.CXPathHelper::fromClass('filter-space').']', 'class:host-dashboard-navigation', 'class:dashboard'] as $selector) { $this->assertTrue($this->query($selector)->exists()); } } public function getCheckFiltersData() { return [ [ [ 'fields' => ['id:from' => 'now-2h', 'id:to' => 'now-1h'], 'expected_tab' => 'now-2h – now-1h', 'zoom_buttons' => [ 'js-btn-time-left' => true, 'btn-time-zoomout' => true, 'js-btn-time-right' => true ] ] ], [ [ 'fields' => ['id:from' => 'now-2y', 'id:to' => 'now-1y'], 'expected_tab' => 'now-2y – now-1y', 'zoom_buttons' => [ 'js-btn-time-left' => true, 'btn-time-zoomout' => true, 'js-btn-time-right' => true ] ] ], [ [ 'link' => 'Last 30 days', 'expected_fields' => ['id:from' => 'now-30d', 'id:to' => 'now'], 'zoom_buttons' => [ 'js-btn-time-left' => true, 'btn-time-zoomout' => true, 'js-btn-time-right' => false ] ] ], [ [ 'link' => 'Last 2 years', 'expected_fields' => ['id:from' => 'now-2y', 'id:to' => 'now'], 'zoom_buttons' => [ 'js-btn-time-left' => true, 'btn-time-zoomout' => false, 'js-btn-time-right' => false ] ] ], [ [ 'fields' => ['id:from' => CURRENT_YEAR.'-01-01 00:00:00', 'id:to' => CURRENT_YEAR.'-01-01 01:00:00'], 'expected_tab' => CURRENT_YEAR.'-01-01 00:00:00 – '.CURRENT_YEAR.'-01-01 01:00:00', 'zoom_buttons' => [ 'js-btn-time-left' => true, 'btn-time-zoomout' => true, 'js-btn-time-right' => true ] ] ], [ [ 'fields' => ['id:from' => '2023-01', 'id:to' => '2023-01'], 'expected_fields' => ['id:from' => '2023-01-01 00:00:00', 'id:to' => '2023-01-31 23:59:59'], 'expected_tab' => '2023-01-01 00:00:00 – 2023-01-31 23:59:59', 'zoom_buttons' => [ 'js-btn-time-left' => true, 'btn-time-zoomout' => true, 'js-btn-time-right' => true ] ] ], [ [ 'fields' => ['id:from' => '$#^$@', 'id:to' => ' '], 'error' => [ 'from' => 'Invalid date.', 'to' => 'Invalid date.' ] ] ], [ [ 'fields' => ['id:from' => 'now-3y', 'id:to' => 'now'], 'error' => [ 'from' => 'Maximum time period to display is {days} days.' ], 'days_count' => true ] ] ]; } /** * Change values in the filter section and check the resulting changes. * * @dataProvider getCheckFiltersData */ public function testDashboardsHostDashboardPage_CheckFilters($data) { $this->openDashboardsForHost(self::HOST_NAME); $filter = CFilterElement::find()->one(); $form = $filter->asForm(['normalized' => true]); // Set custom time filter. if (CTestArrayHelper::get($data, 'fields')) { $form->fill($data['fields']); $form->query('id:apply')->one()->click(); } else { $form->query('link', $data['link'])->waitUntilClickable()->one()->click(); } $this->page->waitUntilReady(); // Check error message if such is expected. if (CTestArrayHelper::get($data, 'error')) { foreach ($data['error'] as $field => $text) { // Count of days mentioned in error depends ot presence of leap year february in selected period. if (CTestArrayHelper::get($data, 'days_count')) { $text = str_replace('{days}', CDateTimeHelper::countDays('now', 'P2Y'), $text); } $message = $this->query('xpath://ul[@data-error-for='.CXPathHelper::escapeQuotes($field).']//li')->one(); $this->assertEquals($text, $message->getText()); } } else { // If error not expected. // Check Zoom buttons. foreach ($data['zoom_buttons'] as $button => $state) { $this->assertTrue($this->query('xpath://button[contains(@class, '.CXPathHelper::escapeQuotes($button). ')]')->one()->isEnabled($state) ); } // Check field values. $form->checkValue(CTestArrayHelper::get($data, 'expected_fields', CTestArrayHelper::get($data, 'fields'))); // Check tab title. $this->assertEquals( CTestArrayHelper::get($data, 'expected_tab', CTestArrayHelper::get($data, 'link')), $filter->getSelectedTabName() ); } } public function getCheckNavigationTabsData() { return [ [ [ 'host_name' => 'One Dashboard - one Page', 'dashboards' => [['name' => 'Dashboard 1']] ] ], [ [ 'host_name' => 'One Dashboard - three Pages', 'dashboards' => [ [ 'name' => 'Dashboard 1', 'pages' => [['name' => 'Page 1'], ['name' => 'Page 2'], ['name' => 'Page 3']] ] ] ] ], [ [ 'host_name' => 'Three Dashboards - three Pages each', 'dashboards' => [ [ 'name' => 'Dashboard 1', 'pages' => [['name' => 'Page 11'], ['name' => 'Page 12'], ['name' => 'Page 13']] ], [ 'name' => 'Dashboard 2', 'pages' => [['name' => 'Page 21'], ['name' => 'Page 22'], ['name' => 'Page 23']] ], [ 'name' => 'Dashboard 3', 'pages' => [['name' => 'Page 31'], ['name' => 'Page 32'], ['name' => 'Page 33']] ] ] ] ], [ [ 'host_name' => 'Unicode Dashboards', 'dashboards' => [ ['name' => '🙂🙃'], ['name' => 'test тест 测试 テスト ทดสอบ'], ['name' => ''], ['name' => '  &'], ['name' => '☺♥²©™"\''] ] ] ], [ [ 'host_name' => 'Unicode Pages', 'dashboards' => [ [ 'name' => 'Dashboard 1', 'pages' => [ ['name' => '🙂🙃'], ['name' => 'test тест 测试 テスト ทดสอบ'], ['name' => ''], ['name' => '  &'], ['name' => '☺♥²©™"\''] ] ] ] ] ], [ [ 'host_name' => 'Long names', 'dashboards' => [ [ 'name' => STRING_255, 'pages' => [['name' => STRING_255], ['name' => STRING_128]] ] ] ] ] ]; } /** * Check Dashboard and Page navigation using tabs. * * @dataProvider getCheckNavigationTabsData */ public function testDashboardsHostDashboardPage_CheckNavigationTabs($data) { // Create the required entities in database. $api_dashboards = $this->createHostWithDashboards($data); $this->openDashboardsForHost($data['host_name']); // Parent to all Dashboard navigation elements. $navigation = $this->query('class:host-dashboard-navigation')->one(); // Assert buttons. $prev_button = $navigation->query('xpath:.//button[@title="Previous dashboard"]')->one(); $this->assertFalse($prev_button->isEnabled()); $next_button = $navigation->query('xpath:.//button[@title="Next dashboard"]')->one(); $this->assertEquals(count($api_dashboards) > 1, $next_button->isEnabled()); // Assert dashboard Tabs and Pages. foreach ($api_dashboards as $dashboard) { $dashboard_tab = $navigation->query('xpath:.//span[text()='.CXPathHelper::escapeQuotes($dashboard['name']).']')->one(); $this->assertEquals($dashboard['name'], $dashboard_tab->getAttribute('title')); // If not already on the correct Dashboard, then switch. if ($dashboard['name'] !== $navigation->query('xpath:.//div[@class="selected-tab"]')->one()->getText()) { $dashboard_tab->click(); $this->page->waitUntilReady(); } /* * Check Page switching. * It is expected that in every page there will be a Widget named like so: 'Dashboard 1 - Page 2 widget'. */ if (count($dashboard['pages']) === 1) { // Case when there is only one Page. The Page button is not even visible. $this->checkDashboardOpen($dashboard); } else { // When a Dashboard contains several Pages. // Check that Slideshow button exists. $this->assertTrue($this->query('class:btn-dashboard-toggle-slideshow')->exists()); // Parent to all Page tabs. $page_tabs = $this->query('class:dashboard-navigation-tabs')->one(); foreach ($dashboard['pages'] as $page) { $page_tab = $page_tabs->query('xpath:.//span[text()='.CXPathHelper::escapeQuotes($page['name']).']')->one(); $this->assertEquals($page['name'], $page_tab->getAttribute('title')); // Only switch the Page if it is not the first one. if ($page['name'] !== $page_tabs->query('xpath:.//div[@class="selected-tab"]')->one()->getText()) { $page_tab->click(); $this->page->waitUntilReady(); } // Assert that the Dashboard has opened. $this->checkDashboardOpen($dashboard, $page); } } } // Assert the Dashboard dropdown. $list_button = $navigation->query('xpath:.//button[@title="Dashboard list"]')->one(); $list_button->click(); $popup_menu = $list_button->asPopupButton()->getMenu(); $this->assertEquals(array_column($api_dashboards, 'name'), $popup_menu->getItems()->asText()); $popup_menu->close(); } public function getCheckNavigationButtonsData() { return [ [ [ 'host_name' => 'Many Dashboards', 'previous_button_selector' => 'class:btn-host-dashboard-previous-dashboard', 'next_button_selector' => 'class:btn-host-dashboard-next-dashboard' ] ], [ [ 'host_name' => 'Many Pages', 'previous_button_selector' => 'class:btn-dashboard-previous-page', 'next_button_selector' => 'class:btn-dashboard-next-page' ] ] ]; } /** * Check Dashboard and Page navigation using the buttons. * * @dataProvider getCheckNavigationButtonsData */ public function testDashboardsHostDashboardPage_CheckNavigationButtons($data) { $this->openDashboardsForHost($data['host_name']); $previous = $this->query($data['previous_button_selector'])->one(); $next = $this->query($data['next_button_selector'])->one(); // If these are set then use them instead of the counter for determining the correct widget name. $dashboard_count = ($data['host_name'] === 'Many Dashboards') ? null : 1; $page_count = ($data['host_name'] === 'Many Pages') ? null : 1; // Cycle tabs in forward direction (by using the > button). for ($i = 1; $i <= self::COUNT_MANY; $i++) { $this->checkDashboardOpen( ['name' => 'Dashboard '.($dashboard_count === null ? $i : $dashboard_count)], ['name' => 'Page '.($page_count === null ? $i : $page_count)] ); //Assert if enabled/disabled correctly. $this->assertEquals($i > 1, $previous->isEnabled()); $this->assertEquals($i < self::COUNT_MANY, $next->isEnabled()); // Only switch the Dashboard if it is not the last one. if ($i !== self::COUNT_MANY) { $next->click(); $this->page->waitUntilReady(); } } // Cycle tabs in backward direction (by using the < button). for ($i = self::COUNT_MANY; $i >= 1; $i--) { $this->checkDashboardOpen( ['name' => 'Dashboard '.($dashboard_count === null ? $i : $dashboard_count)], ['name' => 'Page '.($page_count === null ? $i : $page_count)] ); //Assert if enabled/disabled correctly. $this->assertEquals($i > 1, $previous->isEnabled()); $this->assertEquals($i < self::COUNT_MANY, $next->isEnabled()); // Only switch the Dashboard if it is not the first one. if ($i !== 1) { $previous->click(); $this->page->waitUntilReady(); } } } /** * Check Dashboard navigation using the dropdown. */ public function testDashboardsHostDashboardPage_CheckNavigationDropdown() { // Create a Host with some Dashboards. $data = [ 'host_name' => 'Dashboards for dropdown', 'dashboards' => [ ['name' => 'Dashboard 1'], ['name' => '🙂🙃'], ['name' => ''], ['name' => 'test тест 测试 テスト ทดสอบ'], ['name' => '  &☺♥²©™"\''] ] ]; $api_dashboards = $this->createHostWithDashboards($data); // Open the newly created Host dashboard. $this->openDashboardsForHost('Dashboards for dropdown'); // Click each Dashboard in the menu and assert that it opened. foreach ($api_dashboards as $dashboard) { $navigation = $this->query('class:host-dashboard-navigation')->one(); // Only switch if not already on the correct Dashboard. if ($dashboard['name'] !== $navigation->query('xpath:.//div[@class="selected-tab"]')->one()->getText()) { $list_button = $this->query('xpath:.//button[@title="Dashboard list"]')->one(); $list_button->click(); $list_button->asPopupButton()->getMenu()->select($dashboard['name']); } $this->checkDashboardOpen($dashboard); } } /** * Opens the 'Host dashboards' page for a specific host. * * @param $host_name name of the Host to open Dashboards for */ protected function openDashboardsForHost($host_name) { // Instead of searching the Host in the UI it is faster to just get the ID from the database. $id = CDBHelper::getValue('SELECT hostid FROM hosts WHERE host='.zbx_dbstr($host_name)); $this->page->login()->open('zabbix.php?action=host.dashboard.view&hostid='.$id)->waitUntilReady(); } /** * Creates a Template with required Dashboards using API and assigns it to a new Host. * * @param $data data from data provider * * @returns array dashboard data, that was actually sent to the API (with the defaults set) */ protected function createHostWithDashboards($data) { $response = CDataHelper::createTemplates([ [ 'host' => 'Template for '.$data['host_name'], 'groups' => [ ['groupid' => '1'] // template group 'Templates' ] ] ]); $template_id = $response['templateids']['Template for '.$data['host_name']]; CDataHelper::createHosts([ [ 'host' => $data['host_name'], 'groups' => [ ['groupid' => '6'] // host group 'Virtual machines' ], 'templates' => [ 'templateid' => $template_id ] ] ]); // Add all resulting dashboard data and then return. $api_dashboards = []; foreach ($data['dashboards'] as $dashboard) { // Set Template ID. $dashboard['templateid'] = $template_id; // Add the default Dashboard Page if none set. if (!array_key_exists('pages', $dashboard)) { $dashboard['pages'] = [ [ 'name' => 'Page 1', 'widgets' => [ [ 'type' => 'clock', 'name' => $this->widgetName($dashboard['name'], 'Page 1'), 'width' => 6, 'height' => 4 ] ] ] ]; } // Add default widgets if missing, the name is important. foreach ($dashboard['pages'] as $i => $page) { if (!array_key_exists('widgets', $dashboard['pages'][$i])) { $dashboard['pages'][$i]['widgets'] = [ [ 'type' => 'clock', 'name' => $this->widgetName($dashboard['name'], $page['name']), 'width' => 6, 'height' => 4 ] ]; } } // Create the Dashboard with API. CDataHelper::call('templatedashboard.create', [ $dashboard ]); $api_dashboards[] = $dashboard; } // The dashboard tabs are sorted alphabetically. CTestArrayHelper::usort($api_dashboards, ['name']); return $api_dashboards; } /** * Create a widget name from the dashboard and page name. * The name is used for making sure the correct Dashboard is displayed. * * @param $dashboard_name name of the dashboard this widget is on * @param $page_name name of the page this widget is on * * @return string calculated widget name */ protected function widgetName($dashboard_name, $page_name) { // Widget name max length 255. return substr($dashboard_name.' - '.$page_name.' widget', 0, 255); } /** * Check that the correct Dashboard and Pages is displayed. * This is done by testing for the unique Widget name. * * @param $dashboard Dashboard data array * @param $page Page data array */ protected function checkDashboardOpen ($dashboard, $page = null) { if ($page === null) { $page = $dashboard['pages'][0]; } // Check that the correct Dashboard tab is selected. $navigation = $this->query('class:host-dashboard-navigation')->one(); $this->assertEquals($dashboard['name'], $navigation->query('xpath:.//div[@class="selected-tab"]')->one()->getText()); // Check that the correct Page tab is selected. if (count(CTestArrayHelper::get($dashboard, 'pages', [])) > 1) { $page_tabs = $this->query('class:dashboard-navigation-tabs')->one(); $this->assertEquals($page['name'], $page_tabs->query('xpath:.//div[@class="selected-tab"]')->one()->getText()); } // Assert correct Dashboard is displayed by asserting the Widget name. CDashboardElement::find()->one()->getWidget($this->widgetName($dashboard['name'], $page['name'])); } }