<?php /* ** Zabbix ** Copyright (C) 2001-2025 Zabbix SIA ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. **/ require_once dirname(__FILE__).'/../../include/CWebTest.php'; require_once dirname(__FILE__).'/../behaviors/CMessageBehavior.php'; require_once dirname(__FILE__).'/../behaviors/CTableBehavior.php'; /** * @backup hosts * * @onBefore preparePageHostGroupsData * * @dataSource DiscoveredHosts, HostGroups */ class testPageHostGroups extends CWebTest { /** * Attach MessageBehavior and TableBehavior to the test. * * @return array */ public function getBehaviors() { return [ CMessageBehavior::class, CTableBehavior::class ]; } const LINK = 'hostgroups.php'; const GROUP_DISABLED = 'Group with two disabled hosts testPageHostGroups'; const HOST1 = 'One disabled host testPageHostGroups'; const HOST2 = 'Two disabled host testPageHostGroups'; const TEMPLATE = 'Template with hosts in group testPageHostGroups'; const GROUP_ENABLED = 'Group with two enabled hosts testPageHostGroups'; const DELETE_GROUP3 = 'Group 3 for Delete test'; /** * Objects created in dataSource DiscoveredHosts. */ const DISCOVERED_GROUP = 'Group created from host prototype 1'; const DISCOVERED_GROUP2 = 'Group created from host prototype 11'; const DISCOVERED_HOST = 'Discovered host from prototype 1'; const HOST_PROTOTYPE = 'Host created from host prototype {#KEY}'; const LLD = 'LLD for Discovered host tests'; /** * Objects created in dataSource HostGroups for Delete test. */ const DELETE_ONE_HOST_GROUP = 'One group belongs to one host for Delete test'; const DELETE_ONE_TEMPLATE_GROUP = 'One group belongs to one template for Delete test'; const DELETE_EMPTY_GROUP = 'Group empty for Delete test'; const DELETE_GROUP2 = 'First group to one object for Delete test'; /** * SQL query to get groups and hosts to compare hash values. */ const GROUPS_SQL = 'SELECT * FROM hstgrp g INNER JOIN hosts_groups hg ON g.groupid=hg.groupid'. ' ORDER BY g.groupid, hg.hostgroupid'; const HOSTS_SQL = 'SELECT * FROM hosts ORDER BY hostid'; /** * Prepare data for enable/disable hosts test. */ public static function preparePageHostGroupsData() { // Create three groups with disabled hosts and two groups with enabled hosts for testing. CDataHelper::call('hostgroup.create', [ [ 'name' => self::GROUP_DISABLED ], [ 'name' => 'Group with disabled host testPageHostGroups' ], [ 'name' => 'Group2 with disabled host testPageHostGroups' ], [ 'name' => 'Group with enabled host testPageHostGroups' ], [ 'name' => self::GROUP_ENABLED ], [ 'name' => self::DELETE_GROUP3 ] ]); $groupids = CDataHelper::getIds('name'); CDataHelper::createHosts([ [ 'host' => self::HOST1, 'interfaces' => [], 'status' => HOST_STATUS_NOT_MONITORED, 'groups' => [ 'groupid' => $groupids[self::GROUP_DISABLED] ] ], [ 'host' => self::HOST2, 'interfaces' => [], 'status' => HOST_STATUS_NOT_MONITORED, 'groups' => [ 'groupid' => $groupids[self::GROUP_DISABLED] ] ], [ 'host' => 'Disabled host testPageHostGroups', 'interfaces' => [], 'status' => HOST_STATUS_NOT_MONITORED, 'groups' => [ 'groupid' => $groupids['Group with disabled host testPageHostGroups'] ] ], [ 'host' => 'Disabled host2 testPageHostGroups', 'interfaces' => [], 'status' => HOST_STATUS_NOT_MONITORED, 'groups' => [ 'groupid' => $groupids['Group2 with disabled host testPageHostGroups'] ] ], [ 'host' => 'Enabled host testPageHostGroups', 'interfaces' => [], 'groups' => [ 'groupid' => $groupids['Group with enabled host testPageHostGroups'] ] ], [ 'host' => 'One enabled host testPageHostGroups', 'interfaces' => [], 'groups' => [ 'groupid' => $groupids[self::GROUP_ENABLED] ] ], [ 'host' => 'Two enabled host testPageHostGroups', 'interfaces' => [], 'groups' => [ 'groupid' => $groupids[self::GROUP_ENABLED] ] ] ]); CDataHelper::createTemplates([ [ 'host' => self::TEMPLATE, 'groups' => [ 'groupid' => $groupids[self::GROUP_DISABLED] ] ] ]); } public static function getLayoutData() { return [ [ [ [ 'Name' => 'Discovered hosts', 'Hosts' => 'Hosts', 'Templates' => 'Templates', 'Members' => '', 'Info' => '' ], [ 'Name' => self::LLD.': '.self::DISCOVERED_GROUP, 'Hosts' => 'Hosts 1', 'Templates' => 'Templates', 'Members' => self::DISCOVERED_HOST, 'Info' => '' ], [ 'Name' => self::DELETE_ONE_TEMPLATE_GROUP, 'Hosts' => 'Hosts', 'Templates' => 'Templates 1', 'Members' => 'Template for host group testing', 'Info' => '' ], [ 'Name' => self::GROUP_DISABLED, 'Hosts' => 'Hosts 2', 'Templates' => 'Templates 1', 'Members' => self::TEMPLATE."\n\n".self::HOST1.', '.self::HOST2, 'Info' => '' ] ] ] ]; } /** * @dataProvider getLayoutData */ public function testPageHostGroups_Layout($data) { $this->page->login()->open(self::LINK)->waitUntilReady(); $this->page->assertHeader('Host groups'); $this->page->assertTitle('Configuration of host groups'); // Check filter. $filter = CFilterElement::find()->one(); $form = $filter->getForm(); $this->assertEquals(['Name'], $form->getLabels()->asText()); $this->assertTrue($form->getField('Name')->isAttributePresent(['value' => '', 'maxlength' => '255'])); // Check displaying and hiding the filter container. $this->assertTrue($filter->isExpanded()); foreach ([false, true] as $state) { $filter->expand($state); // Leave the page and reopen the previous page to make sure the filter state is still saved. $this->page->open('zabbix.php?action=report.status')->waitUntilReady(); $this->page->open(self::LINK)->waitUntilReady(); $this->assertTrue($filter->isExpanded($state)); } // Check buttons. $this->assertEquals(3, $this->query('button', ['Create host group', 'Apply', 'Reset']) ->all()->filter(CElementFilter::CLICKABLE)->count()); $this->assertEquals(3, $this->query('button', ['Enable hosts', 'Disable hosts', 'Delete']) ->all()->filter(CElementFilter::NOT_CLICKABLE)->count() ); // Check table headers. $table = $this->getTable(); $this->assertEquals(['' , 'Name', 'Hosts', 'Templates', 'Members', 'Info'] , $table->getHeadersText()); $this->assertEquals(['Name'], $table->getSortableHeaders()->asText()); // Check the displayed number of groups in the table. $names = $this->getGroupNames(); $this->assertTableStats(count($names)); $this->assertSelectedCount(0); $this->selectTableRows(); $this->assertSelectedCount(count($names)); $this->query('id:all_groups')->asCheckbox()->one()->uncheck(); $this->assertSelectedCount(0); // Check table content. $this->assertTableHasData($data); // Check hintbox of discovered host group in info column. $hintbox_row = $table->findRow('Name', self::LLD.': '.self::DISCOVERED_GROUP); $hintbox_row->query('xpath://a[@class="icon-info status-yellow"]')->one()->click(); $hintbox = $form->query('xpath://div[@class="overlay-dialogue wordbreak"]')->waitUntilVisible(); $this->assertEquals('The host group is not discovered anymore and will be deleted the next time discovery'. ' rule is processed.', $hintbox->one()->getText() ); $hintbox->query('class:overlay-close-btn')->one()->click()->waitUntilNotPresent(); } public static function getLinksData() { return [ [ [ 'name' => self::DISCOVERED_GROUP, 'lld' => self::LLD, 'host' => self::DISCOVERED_HOST ] ], [ [ 'name' => 'Group for discovered host test', 'host' => self::DISCOVERED_HOST ] ], [ [ 'name' => self::GROUP_DISABLED, 'host' => self::HOST1, 'template' => self::TEMPLATE ] ], [ [ 'name' => self::DELETE_ONE_TEMPLATE_GROUP, 'template' => 'Template for host group testing' ] ] ]; } /** * Check related links of group in table row. * * @dataProvider getLinksData */ public function testPageHostGroups_Links($data) { $this->page->login()->open(self::LINK)->waitUntilReady(); $table = $this->getTable(); $row = $table->findRow('Name', array_key_exists('lld', $data) ? $data['lld'].': '.$data['name'] : $data['name']); // Check link to the host or template edit form. if (array_key_exists('host', $data)) { $id = CDBHelper::getValue('SELECT hostid FROM hosts WHERE host='.zbx_dbstr($data['host'])); $row->getColumn('Members')->query('link', $data['host'])->one()->click(); $dialog = COverlayDialogElement::find()->one()->waitUntilReady(); $this->assertStringContainsString('zabbix.php?action=host.edit&hostid='.$id, $this->page->getCurrentUrl()); $this->assertEquals('Host', $dialog->getTitle()); $dialog->asForm()->checkValue(['Host name' => $data['host']]); $dialog->close(); } if (array_key_exists('template', $data)) { $id = CDBHelper::getValue('SELECT hostid FROM hosts WHERE host='.zbx_dbstr($data['template'])); $row->getColumn('Members')->query('link', $data['template'])->one()->click(); $this->assertStringContainsString('templates.php?form=update&templateid='.$id, $this->page->getCurrentUrl()); $this->page->assertHeader('Templates'); $this->query('id:templates-form')->asForm()->waitUntilVisible()->one() ->checkValue(['Template name' => $data['template']]); $this->query('button:Cancel')->one()->click(); $this->assertStringContainsString('templates.php', $this->page->getCurrentUrl()); $this->page->open(self::LINK)->waitUntilReady(); } // Check link to hosts or templates page with selected group in filer. $group_id = CDBHelper::getValue('SELECT groupid FROM hstgrp WHERE name='.zbx_dbstr($data['name'])); foreach (['Hosts', 'Templates'] as $object) { $column = $row->getColumn($object); $count_tag = $column->query('tag:sup')->one(false); $count = $count_tag->isValid() ? $count_tag->getText() : 0; $column->query('link', $object)->one()->click(); $this->assertStringContainsString((($object === 'Hosts') ? 'zabbix.php?action=host.list&' : 'templates.php?'). 'filter_set=1&filter_groups%5B0%5D='.$group_id, $this->page->getCurrentUrl() ); $this->page->assertHeader($object); $filter_form = CFilterElement::find()->one()->getForm(); $filter_form->checkValue(['Host groups' => $data['name']]); $this->assertTableStats($count); $this->page->open(self::LINK)->waitUntilReady(); } // Check link to host prototype from host group name. if (array_key_exists('lld', $data)) { $row->getColumn('Name')->query('link', $data['lld'])->one()->click(); $this->assertStringContainsString('host_prototypes.php?form=update&parent_discoveryid=', $this->page->getCurrentUrl()); $this->page->assertHeader('Host prototypes'); $this->query('id:host-prototype-form')->asForm()->waitUntilVisible()->one() ->checkValue(['Host name' => self::HOST_PROTOTYPE]); $this->query('button:Cancel')->one()->click(); $this->assertStringContainsString('host_prototypes.php?cancel=1&parent_discoveryid=', $this->page->getCurrentUrl()); $this->page->open(self::LINK)->waitUntilReady(); } } /** * Get and sort group names. * * @param string $sort sort content ascending or descending */ private function getGroupNames($sort = 'asc') { $names = CDBHelper::getColumn('SELECT name FROM hstgrp', 'name'); natcasesort($names); if ($sort !== 'asc') { $names = array_reverse($names); } // Change names of discovered groups on page. $names[array_search(self::DISCOVERED_GROUP, $names)] = self::LLD.': '.self::DISCOVERED_GROUP; $names[array_search(self::DISCOVERED_GROUP2, $names)] = self::LLD.': '.self::DISCOVERED_GROUP2; return $names; } /** * Check ascending and descending groups sorting by column Name. */ public function testPageHostGroups_Sort() { $this->page->login()->open(self::LINK)->waitUntilReady(); $table = $this->getTable(); foreach (['desc', 'asc'] as $sorting) { $names = $this->getGroupNames($sorting); $table->query('link:Name')->waitUntilClickable()->one()->click(); $table->waitUntilReloaded(); $this->assertTableDataColumn($names); } } public static function getFilterData() { return [ // Special symbols, utf8 and long name. [ [ 'Name' => '&<>//\\[]""#@' ] ], [ [ 'Name' => 'æ㓴🙂' ] ], [ [ 'Name' => STRING_255 ] ], // Exact match. [ [ 'Name' => 'Group with disabled host testPageHostGroups', 'expected' => ['Group with disabled host testPageHostGroups'] ] ], [ [ 'Name' => self::DELETE_ONE_TEMPLATE_GROUP, 'expected' => [self::DELETE_ONE_TEMPLATE_GROUP] ] ], // Partial match. [ [ 'Name' => 'with two enabled hosts', 'expected' => [self::GROUP_ENABLED] ] ], [ [ 'Name' => ' enabled ', 'expected' => ['Group with enabled host testPageHostGroups', self::GROUP_ENABLED] ] ], [ [ 'Name' => 'with disabled', 'expected' => ['Group2 with disabled host testPageHostGroups', 'Group with disabled host testPageHostGroups'] ] ], // Space trimming. [ [ 'Name' => ' enabled ' ] ], [ [ 'Name' => ' ' ] ], // Not case sensitive. [ [ 'Name' => 'group2', 'expected' => ['Group2 with disabled host testPageHostGroups'] ] ], [ [ 'Name' => 'GROUP2', 'expected' => ['Group2 with disabled host testPageHostGroups'] ] ] ]; } /** * @dataProvider getFilterData */ public function testPageHostGroups_Filter($data) { $all = $this->getGroupNames(); if (array_key_exists('all', $data)) { $data['expected'] = $all; } $this->page->login()->open(self::LINK)->waitUntilReady(); $table = $this->getTable(); $form = CFilterElement::find()->one()->getForm(); $form->fill(['Name' => $data['Name']]); $form->submit(); $table->waitUntilReloaded(); $this->assertTableStats(count(CTestArrayHelper::get($data, 'expected', []))); $this->assertTableDataColumn(CTestArrayHelper::get($data, 'expected', [])); // Reset filter. $form->query('button:Reset')->one()->click(); $table->waitUntilReloaded(); $this->assertTableStats(count($all)); } public static function getHostGroupsCancelData() { return [ [ [ 'action' => 'Enable hosts', 'message' => 'Enable selected hosts?' ] ], [ [ 'action' => 'Disable hosts', 'message' => 'Disable hosts in the selected host groups?' ] ], [ [ 'action' => 'Delete', 'message' => 'Delete selected host groups?' ] ] ]; } /** * @dataProvider getHostGroupsCancelData */ public function testPageHostGroups_Cancel($data) { $rows = [ [ 'index' => 0, 'count' => 1 ], [ 'index' => 1, 'count' => 2 ], [ 'all' => true ] ]; $old_grpups_hash = CDBHelper::getHash(self::GROUPS_SQL); $old_hosts_hash = CDBHelper::getHash(self::HOSTS_SQL); $this->page->login()->open(self::LINK)->waitUntilReady(); $table = $this->getTable(); foreach ($rows as $row) { if (array_key_exists('all', $row)) { $row['count'] = count($this->getGroupNames()); $this->selectTableRows(); } else { $table->getRow($row['index'])->select(); } $this->assertSelectedCount($row['count']); $this->query('button', $data['action'])->one()->click(); $this->assertEquals($data['message'], $this->page->getAlertText()); $this->page->dismissAlert(); $this->assertSelectedCount($row['count']); } $this->assertEquals($old_grpups_hash, CDBHelper::getHash(self::GROUPS_SQL)); $this->assertEquals($old_hosts_hash, CDBHelper::getHash(self::HOSTS_SQL)); } /** * Check that status of host is changed in groups table when change host status in overlay dialog on host groups page. */ public function testPageHostGroups_SingleEnableDisable() { $data = [ 'group' => self::GROUP_DISABLED, // Group linked to two hosts, but only one host's status will change. 'change_host' => self::HOST1, 'host' => self::HOST2, 'template' => self::TEMPLATE ]; $this->page->login()->open(self::LINK)->waitUntilReady(); $table = $this->getTable(); $hosts = $table->findRow('Name', $data['group'])->getColumn('Members'); foreach ([true, false] as $status) { // Change host status. $hosts->query('link', $data['change_host'])->one()->click(); $dialog = COverlayDialogElement::find()->one()->waitUntilReady(); $dialog->asForm()->fill(['Enabled' => $status])->submit(); $dialog->ensureNotPresent(); $table->waitUntilReloaded(); $this->assertMessage(TEST_GOOD, 'Host updated', 'Updated status of host "'.$data['change_host'].'".'); CMessageElement::find()->one()->close(); // Check status in table. foreach ([$data['change_host'] => $status ? 'green' : 'red', $data['host'] => 'red', $data['template'] => 'grey'] as $host => $class) { $this->assertTrue($hosts->query('link', $host)->one()->hasClass($class)); } // Check status in DB. $db_status = $status ? HOST_STATUS_MONITORED : HOST_STATUS_NOT_MONITORED; $this->assertEquals(1, CDBHelper::getCount('SELECT NULL FROM hosts WHERE status='.$db_status. ' AND name='.CDBHelper::escape($data['change_host'])) ); $this->assertEquals(1, CDBHelper::getCount('SELECT NULL FROM hosts WHERE status='.HOST_STATUS_NOT_MONITORED. ' AND name='.CDBHelper::escape($data['host'])) ); } } public static function getEnableHostsData() { return [ [ [ 'Discovered hosts' => '' ] ], [ [ 'Group with disabled host testPageHostGroups' => ['Disabled host testPageHostGroups'] ] ], [ [ 'Group2 with disabled host testPageHostGroups' => ['Disabled host2 testPageHostGroups'], self::GROUP_DISABLED => [self::HOST1, self::HOST2] ] ] ]; } /** * @dataProvider getEnableHostsData */ public function testPageHostGroups_EnableHosts($data) { $this->checkHostStatusChange($data); } public static function getDisableHostsData() { return [ [ [ 'Group for Host prototype' => '' ] ], [ [ 'Group with enabled host testPageHostGroups' => ['Enabled host testPageHostGroups'] ] ], [ [ self::LLD.': '.self::DISCOVERED_GROUP => [self::DISCOVERED_HOST], self::GROUP_ENABLED => ['One enabled host testPageHostGroups', 'Two enabled host testPageHostGroups'] ] ] ]; } /** * @dataProvider getDisableHostsData */ public function testPageHostGroups_DisableHosts($data) { $this->checkHostStatusChange($data, 'disable'); } /** * Check that hosts are enabled or disabled when performing an action on a host group. * * @param array $data data provider * @param string $status enable or disable hosts */ private function checkHostStatusChange($data, $status = 'enable') { if (count($data) === 1 && array_values($data)[0] === '') { $old_hash = CDBHelper::getHash(self::HOSTS_SQL); } $this->page->login()->open(self::LINK)->waitUntilReady(); $table = $this->getTable(); $this->selectTableRows(array_keys($data)); $this->assertSelectedCount(count($data)); $this->query('button', ucfirst($status).' hosts')->one()->click(); $this->page->acceptAlert(); $this->page->waitUntilReady(); $table->waitUntilReloaded(); $this->assertSelectedCount(0); $details = []; foreach ($data as $group => $hosts) { // Skip checks if group without hosts. if (!is_array($hosts)) { $this->assertEquals($old_hash, CDBHelper::getHash(self::HOSTS_SQL)); continue; } $row = $table->findRow('Name', $group); // Check hosts status in DB. $db_status = ($status === 'enable') ? HOST_STATUS_MONITORED : HOST_STATUS_NOT_MONITORED; $this->assertEquals(count($hosts), CDBHelper::getCount('SELECT NULL FROM hosts WHERE status='. $db_status.' AND name IN ('.CDBHelper::escape($hosts).')') ); // Check template color on frontend. if ($group === self::GROUP_DISABLED) { $this->assertTrue($row->getColumn('Members')->query('link', self::TEMPLATE)->one()->hasClass('grey')); } foreach ($hosts as $host) { // Check hosts color on frontend. $host_link = $row->getColumn('Members')->query('link', $host)->one(); $this->assertTrue($host_link->hasClass(($status === 'enable') ? 'green' : 'red')); // Prepare message details text. $details[] = 'Updated status of host "'.$host.'".'; } } $message_title = (count($details) === 1) ? 'Host '.$status.'d' : 'Hosts '.$status.'d'; $this->assertMessage(TEST_GOOD, $message_title, $details); } public static function getHostGroupsDeleteData() { return [ // Delete all. [ [ 'expected' => TEST_BAD, 'error' => 'Host group "Discovered hosts" is internal and cannot be deleted.' ] ], // One of the groups can't be deleted. [ [ 'expected' => TEST_BAD, 'groups' => [self::DELETE_ONE_HOST_GROUP, self::LLD.': '.self::DISCOVERED_GROUP], 'error' => 'Host "Host for host group testing" cannot be without host group.' ] ], // The group can't be deleted. [ [ 'expected' => TEST_BAD, 'groups' => self::DELETE_ONE_HOST_GROUP, 'error' => 'Host "Host for host group testing" cannot be without host group.' ] ], [ [ 'expected' => TEST_BAD, 'groups' => self::DELETE_ONE_TEMPLATE_GROUP, 'error' => 'Template "Template for host group testing" cannot be without host group.' ] ], // Group used in other elements can't be deleted. [ [ 'expected' => TEST_BAD, 'groups' => 'Group for Maintenance', 'error' => 'Cannot delete host group "Group for Maintenance" because maintenance'. ' "Maintenance for host group testing" must contain at least one host or host group.' ] ], [ [ 'expected' => TEST_BAD, 'groups' => 'Group for Correlation', 'error' => 'Group "Group for Correlation" cannot be deleted, because it is used in a correlation condition.' ] ], [ [ 'expected' => TEST_BAD, 'groups' => 'Group for Script', 'error' => 'Host group "Group for Script" cannot be deleted, because it is used in a global script.' ] ], [ [ 'expected' => TEST_BAD, 'groups' => 'Group for Host prototype', 'error' => 'Group "Group for Host prototype" cannot be deleted, because it is used by a host prototype.' ] ], [ [ 'expected' => TEST_BAD, 'groups' => 'Discovered hosts', 'error' => 'Host group "Discovered hosts" is internal and cannot be deleted.' ] ], // Select one. [ [ 'expected' => TEST_GOOD, 'groups' => self::DELETE_EMPTY_GROUP ] ], // Select several. [ [ 'expected' => TEST_GOOD, 'groups' => [self::DELETE_GROUP2, self::DELETE_GROUP3, 'Group for Action'] ] ] ]; } /** * @dataProvider getHostGroupsDeleteData */ public function testPageHostGroups_Delete($data) { if ($data['expected'] === TEST_BAD) { $old_hash = CDBHelper::getHash(self::GROUPS_SQL); } if (!is_array(CTestArrayHelper::get($data, 'groups', []))) { $data['groups'] = [$data['groups']]; } $all = $this->getGroupNames(); $count = count(CTestArrayHelper::get($data, 'groups', $all)); $this->page->login()->open(self::LINK)->waitUntilReady(); $table = $this->getTable(); $this->selectTableRows(CTestArrayHelper::get($data, 'groups')); $this->query('button:Delete')->one()->click(); $this->page->acceptAlert(); $table->waitUntilReloaded(); if ($data['expected'] === TEST_GOOD) { $this->assertSelectedCount(0); $this->assertTableStats(count($all) - $count); $this->assertMessage(TEST_GOOD, (($count === 1) ? 'Group' : 'Groups').' deleted'); $this->assertEquals(0, CDBHelper::getCount('SELECT NULL FROM hstgrp WHERE name IN ('. CDBHelper::escape($data['groups']).')') ); } else { $this->assertSelectedCount($count); $this->assertMessage(TEST_BAD, 'Cannot delete group'.(($count > 1) ? 's' : ''), $data['error']); $this->assertEquals($old_hash, CDBHelper::getHash(self::GROUPS_SQL)); // Reset selected groups. $this->query('button:Reset')->one()->click(); $table->waitUntilReloaded(); $this->assertTableStats(count($all)); } } }