mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 08:09:27 +00:00
chore: incidents as separate tabs (#9819)
### **PR Type**
Enhancement, Tests
___
### **Description**
- Replace icon tabs with reusable app-tabs component
- Introduce Alerts/Incidents viewMode switching logic
- Add extensive UI tests for new tab functionality
- Update getPath logic and dev script configuration
___
### Diagram Walkthrough
```mermaid
flowchart LR
TabsComp["app-tabs component"] --> ViewState["viewMode state"]
ViewState --> AlertsView["Alerts view UI"]
ViewState --> IncidentsView["Incidents view UI"]
Tests["UI tests"] --> TabsComp
```
<details> <summary><h3> File Walkthrough</h3></summary>
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Bug
fix</strong></td><td><table>
<tr>
<td>
<details>
<summary><strong>zincutils.ts</strong><dd><code>Refine getPath base URL
logic</code>
</dd></summary>
<hr>
web/src/utils/zincutils.ts
<ul><li>refine getPath logic for web base path<br> <li> conditionally
prepend "/web/" on localhost</ul>
</details>
</td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9819/files#diff-d3d59688cccdcc6c84ebe88b698c6910030abecc3125f4c0ef81b0df023699ba">+1/-1</a>
</td>
</tr>
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
<td>
<details>
<summary><strong>AlertList.vue</strong><dd><code>Replace custom tabs
with app-tabs</code>
</dd></summary>
<hr>
web/src/components/alerts/AlertList.vue
<ul><li>remove custom icon-only view tabs<br> <li> integrate
<code>app-tabs</code> for view switching<br> <li> add
<code>viewTabs</code> computed property<br> <li> update related SCSS
styling</ul>
</details>
</td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9819/files#diff-be3e41b823e163b226ffc270d95cc4981e59c600851e4e0d10ee5e4ee994ac8e">+35/-92</a>
</td>
</tr>
</table></td></tr><tr><td><strong>Tests</strong></td><td><table>
<tr>
<td>
<details>
<summary><strong>alertsPage.js</strong><dd><code>Add view mode tab
methods and locators</code>
</dd></summary>
<hr>
tests/ui-testing/pages/alertsPages/alertsPage.js
<ul><li>add locators for alert and incident tabs<br> <li> implement
click and expect methods for tabs<br> <li> add search methods for both
views</ul>
</details>
</td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9819/files#diff-79ee01d6d7f145f19207b93b87143b545a1188cfc6a82bc8c45fecfbf68f8a1d">+113/-0</a>
</td>
</tr>
<tr>
<td>
<details>
<summary><strong>alertsIncidentsTabs.spec.js</strong><dd><code>Add E2E
tests for alerts/incidents tabs</code>
</dd></summary>
<hr>
tests/ui-testing/playwright-tests/Alerts/alertsIncidentsTabs.spec.js
<ul><li>add E2E tests for tabs visibility and switching<br> <li> cover
functional and edge-case scenarios</ul>
</details>
</td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9819/files#diff-3776665660a181de2d08e4d3bccc481191e7ac88f4e4b0943c869ffa4eb12ffd">+347/-0</a>
</td>
</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
<td>
<details>
<summary><strong>package.json</strong><dd><code>Add base path to dev
script</code>
</dd></summary>
<hr>
web/package.json
- update `dev` script to include base `/web/`
</details>
</td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9819/files#diff-b861012a5dd72b8a9f3281b7cf09f5a779c98569d040b1bbc1db50f1b15e7cce">+1/-1</a>
</td>
</tr>
</table></td></tr></tr></tbody></table>
</details>
___
This commit is contained in:
@@ -142,6 +142,18 @@ export class AlertsPage {
|
||||
searchAcrossFoldersToggle: '[data-test="alert-list-search-across-folders-toggle"]',
|
||||
alertDeleteOption: 'Delete',
|
||||
|
||||
// View Mode Tabs (Alerts / Incidents) - VERIFIED from source code
|
||||
alertIncidentViewTabs: '[data-test="alert-incident-view-tabs"]',
|
||||
alertsViewTab: '[data-test="tab-alerts"]',
|
||||
incidentsViewTab: '[data-test="tab-incidents"]',
|
||||
|
||||
// Incidents view locators
|
||||
incidentListTable: '[data-test="incident-list-table"]',
|
||||
incidentSearchInput: '[data-test="incident-search-input"]',
|
||||
|
||||
// Import button
|
||||
alertImportButton: '[data-test="alert-import"]',
|
||||
|
||||
// Table locators
|
||||
tableBodyRowWithIndex: 'tbody tr[data-index]',
|
||||
tableLocator: 'table',
|
||||
@@ -630,6 +642,107 @@ export class AlertsPage {
|
||||
await expect(this.page.locator('[data-test="dashboard-folder-tab-default"]').getByText('default')).toBeVisible();
|
||||
}
|
||||
|
||||
// ==================== VIEW MODE TABS (ALERTS / INCIDENTS) ====================
|
||||
|
||||
/**
|
||||
* Click the Alerts view tab
|
||||
*/
|
||||
async clickAlertsTab() {
|
||||
testLogger.info('Clicking Alerts tab');
|
||||
await this.page.locator(this.locators.alertsViewTab).click();
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
||||
testLogger.info('Switched to Alerts view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the Incidents view tab
|
||||
*/
|
||||
async clickIncidentsTab() {
|
||||
testLogger.info('Clicking Incidents tab');
|
||||
await this.page.locator(this.locators.incidentsViewTab).click();
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
||||
testLogger.info('Switched to Incidents view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the view mode tabs are visible on page load
|
||||
*/
|
||||
async expectViewModeTabsVisible() {
|
||||
testLogger.info('Verifying view mode tabs are visible');
|
||||
await expect(this.page.locator(this.locators.alertIncidentViewTabs)).toBeVisible({ timeout: 10000 });
|
||||
await expect(this.page.locator(this.locators.alertsViewTab)).toBeVisible();
|
||||
await expect(this.page.locator(this.locators.incidentsViewTab)).toBeVisible();
|
||||
testLogger.info('View mode tabs verified');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Alerts view UI elements are visible
|
||||
*/
|
||||
async expectAlertsViewElementsVisible() {
|
||||
testLogger.info('Verifying Alerts view elements');
|
||||
await expect(this.page.locator('[data-test="alert-list-table"]')).toBeVisible({ timeout: 10000 });
|
||||
await expect(this.page.locator(this.locators.alertSearchInput)).toBeVisible();
|
||||
await expect(this.page.locator(this.locators.searchAcrossFoldersToggle)).toBeVisible();
|
||||
await expect(this.page.locator(this.locators.alertImportButton)).toBeVisible();
|
||||
await expect(this.page.locator(this.locators.addAlertButton)).toBeVisible();
|
||||
testLogger.info('Alerts view elements verified');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Incidents view UI elements are visible
|
||||
*/
|
||||
async expectIncidentsViewElementsVisible() {
|
||||
testLogger.info('Verifying Incidents view elements');
|
||||
await expect(this.page.locator(this.locators.incidentListTable)).toBeVisible({ timeout: 10000 });
|
||||
await expect(this.page.locator(this.locators.incidentSearchInput)).toBeVisible();
|
||||
testLogger.info('Incidents view elements verified');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Alerts-only elements are hidden in Incidents view
|
||||
*/
|
||||
async expectAlertsOnlyElementsHidden() {
|
||||
testLogger.info('Verifying Alerts-only elements are hidden');
|
||||
await expect(this.page.locator('[data-test="alert-list-table"]')).not.toBeVisible();
|
||||
await expect(this.page.locator(this.locators.searchAcrossFoldersToggle)).not.toBeVisible();
|
||||
await expect(this.page.locator(this.locators.alertImportButton)).not.toBeVisible();
|
||||
await expect(this.page.locator(this.locators.addAlertButton)).not.toBeVisible();
|
||||
testLogger.info('Alerts-only elements confirmed hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Incidents-only elements are hidden in Alerts view
|
||||
*/
|
||||
async expectIncidentsOnlyElementsHidden() {
|
||||
testLogger.info('Verifying Incidents-only elements are hidden');
|
||||
await expect(this.page.locator(this.locators.incidentListTable)).not.toBeVisible();
|
||||
await expect(this.page.locator(this.locators.incidentSearchInput)).not.toBeVisible();
|
||||
testLogger.info('Incidents-only elements confirmed hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in Alerts view
|
||||
*/
|
||||
async searchInAlertsView(query) {
|
||||
testLogger.info(`Searching in Alerts view: ${query}`);
|
||||
const searchInput = this.page.locator(this.locators.alertSearchInput);
|
||||
await searchInput.click();
|
||||
await searchInput.clear();
|
||||
await searchInput.type(query);
|
||||
await this.page.waitForTimeout(500); // Wait for debounce
|
||||
testLogger.info('Search applied in Alerts view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in Incidents view
|
||||
*/
|
||||
async searchInIncidentsView(query) {
|
||||
testLogger.info(`Searching in Incidents view: ${query}`);
|
||||
await this.page.locator(this.locators.incidentSearchInput).fill(query);
|
||||
await this.page.waitForTimeout(500); // Wait for debounce
|
||||
testLogger.info('Search applied in Incidents view');
|
||||
}
|
||||
|
||||
// ==================== IMPORT/EXPORT OPERATIONS ====================
|
||||
|
||||
async exportAlerts() {
|
||||
|
||||
@@ -25,7 +25,8 @@ export class CommonActions {
|
||||
async navigateToAlerts() {
|
||||
await this.page.locator(this.alertsMenuItem).click();
|
||||
await this.page.waitForTimeout(2000);
|
||||
await expect(this.page.locator('[data-test="alerts-list-title"]')).toContainText('Alerts');
|
||||
// Updated to check for view mode tabs instead of old title element
|
||||
await expect(this.page.locator('[data-test="alert-incident-view-tabs"]')).toBeVisible();
|
||||
}
|
||||
|
||||
async navigateToSettings() {
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* Alerts & Incidents Tab Separation - E2E Tests
|
||||
*
|
||||
* Test Suite: Validates the new tab-based UI for separating Alerts and Incidents views
|
||||
* Module: Alerts
|
||||
* Priority: High (Core UX Enhancement)
|
||||
*
|
||||
* Feature: Users can switch between Alerts (configuration) and Incidents (events) using
|
||||
* distinct tabs with proper conditional rendering of view-specific UI elements.
|
||||
*
|
||||
* Test Coverage:
|
||||
* - P0 (Smoke): Tabs visible, default view, tab switching
|
||||
* - P1 (Functional): View-specific elements, search functionality
|
||||
* - P2 (Edge Cases): Empty states, page refresh
|
||||
*/
|
||||
|
||||
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
|
||||
const testLogger = require('../utils/test-logger.js');
|
||||
const PageManager = require('../../pages/page-manager.js');
|
||||
const logData = require('../../fixtures/log.json');
|
||||
|
||||
test.describe("Alerts & Incidents Tab Separation", () => {
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
let pm;
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testLogger.testStart(testInfo.title, testInfo.file);
|
||||
await navigateToBase(page);
|
||||
pm = new PageManager(page);
|
||||
|
||||
// Navigate to alerts page
|
||||
await page.goto(`${logData.logsUrl}?org_identifier=${process.env["ORGNAME"]}`);
|
||||
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
||||
testLogger.info('Navigated to homepage');
|
||||
|
||||
// Wait for any loading overlays to disappear (wait up to 30 seconds)
|
||||
const loadingOverlay = page.locator('.fullscreen.bg-blue');
|
||||
if (await loadingOverlay.isVisible().catch(() => false)) {
|
||||
await loadingOverlay.waitFor({ state: 'hidden', timeout: 30000 }).catch(() => {});
|
||||
}
|
||||
|
||||
// Additional wait to ensure page is ready
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Click alerts menu item
|
||||
await page.locator(pm.alertsPage.locators.alertMenuItem).click();
|
||||
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
||||
testLogger.info('Navigated to alerts page');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// P0 - SMOKE TESTS (Critical Path)
|
||||
// ========================================
|
||||
|
||||
test("TC-01: Tabs visible on page load", {
|
||||
tag: ['@alertsIncidentsTabs', '@smoke', '@P0', '@all']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P0] Testing tabs visibility on page load');
|
||||
|
||||
// Verify tabs container and individual tabs are visible
|
||||
await pm.alertsPage.expectViewModeTabsVisible();
|
||||
|
||||
testLogger.info('[P0] ✓ Tabs verified visible on page load');
|
||||
});
|
||||
|
||||
test("TC-02: Default view is Alerts", {
|
||||
tag: ['@alertsIncidentsTabs', '@smoke', '@P0', '@all']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P0] Testing default view is Alerts');
|
||||
|
||||
// Verify Alerts view UI elements are visible
|
||||
await pm.alertsPage.expectAlertsViewElementsVisible();
|
||||
|
||||
// Verify Incidents-only elements are hidden
|
||||
await pm.alertsPage.expectIncidentsOnlyElementsHidden();
|
||||
|
||||
testLogger.info('[P0] ✓ Default view confirmed as Alerts');
|
||||
});
|
||||
|
||||
test("TC-03: Tab switching works (Alerts → Incidents)", {
|
||||
tag: ['@alertsIncidentsTabs', '@smoke', '@P0', '@all']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P0] Testing tab switch from Alerts to Incidents');
|
||||
|
||||
// Click Incidents tab
|
||||
await pm.alertsPage.clickIncidentsTab();
|
||||
|
||||
// Verify Incidents view elements are visible
|
||||
await pm.alertsPage.expectIncidentsViewElementsVisible();
|
||||
|
||||
// Verify Alerts-only elements are hidden
|
||||
await pm.alertsPage.expectAlertsOnlyElementsHidden();
|
||||
|
||||
testLogger.info('[P0] ✓ Tab switch to Incidents successful');
|
||||
});
|
||||
|
||||
test("TC-04: Tab switching works (Incidents → Alerts)", {
|
||||
tag: ['@alertsIncidentsTabs', '@smoke', '@P0', '@all']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P0] Testing tab switch from Incidents back to Alerts');
|
||||
|
||||
// First switch to Incidents
|
||||
await pm.alertsPage.clickIncidentsTab();
|
||||
testLogger.info('Switched to Incidents view');
|
||||
|
||||
// Then switch back to Alerts
|
||||
await pm.alertsPage.clickAlertsTab();
|
||||
|
||||
// Verify Alerts view elements are visible again
|
||||
await pm.alertsPage.expectAlertsViewElementsVisible();
|
||||
|
||||
// Verify Incidents-only elements are hidden again
|
||||
await pm.alertsPage.expectIncidentsOnlyElementsHidden();
|
||||
|
||||
testLogger.info('[P0] ✓ Tab switch back to Alerts successful');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// P1 - FUNCTIONAL TESTS
|
||||
// ========================================
|
||||
|
||||
test("TC-05: Alerts view shows all required UI elements", {
|
||||
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P1] Testing Alerts view UI elements');
|
||||
|
||||
// Verify all Alerts view elements
|
||||
await expect(page.locator('[data-test="alert-list-table"]')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator(pm.alertsPage.locators.alertSearchInput)).toBeVisible();
|
||||
await expect(page.locator(pm.alertsPage.locators.searchAcrossFoldersToggle)).toBeVisible();
|
||||
await expect(page.locator(pm.alertsPage.locators.alertImportButton)).toBeVisible();
|
||||
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).toBeVisible();
|
||||
|
||||
// Verify folders panel (splitter) is visible
|
||||
await expect(page.locator('[data-test="alert-list-splitter"]')).toBeVisible();
|
||||
|
||||
testLogger.info('[P1] ✓ All Alerts view UI elements verified');
|
||||
});
|
||||
|
||||
test("TC-06: Incidents view shows only incident UI elements", {
|
||||
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P1] Testing Incidents view UI elements');
|
||||
|
||||
// Switch to Incidents view
|
||||
await pm.alertsPage.clickIncidentsTab();
|
||||
|
||||
// Verify Incidents view elements visible
|
||||
await pm.alertsPage.expectIncidentsViewElementsVisible();
|
||||
|
||||
// Verify Alerts-only elements are NOT visible
|
||||
await expect(page.locator('[data-test="alert-list-table"]')).not.toBeVisible();
|
||||
await expect(page.locator(pm.alertsPage.locators.searchAcrossFoldersToggle)).not.toBeVisible();
|
||||
await expect(page.locator(pm.alertsPage.locators.alertImportButton)).not.toBeVisible();
|
||||
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).not.toBeVisible();
|
||||
|
||||
testLogger.info('[P1] ✓ Incidents view UI elements verified');
|
||||
});
|
||||
|
||||
test("TC-07: Search works in Alerts view", {
|
||||
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all', '@search']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P1] Testing search in Alerts view');
|
||||
|
||||
// Type in alert search input
|
||||
const searchQuery = 'test';
|
||||
const searchInput = page.locator(pm.alertsPage.locators.alertSearchInput);
|
||||
|
||||
// Click and focus the input
|
||||
await searchInput.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Type the search query
|
||||
await searchInput.pressSequentially(searchQuery, { delay: 100 });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify search functionality works (input accepts text and table is visible)
|
||||
await expect(searchInput).toBeFocused();
|
||||
await expect(page.locator('[data-test="alert-list-table"]')).toBeVisible();
|
||||
|
||||
testLogger.info('[P1] ✓ Search in Alerts view verified');
|
||||
});
|
||||
|
||||
test("TC-08: Search works in Incidents view", {
|
||||
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all', '@search']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P1] Testing search in Incidents view');
|
||||
|
||||
// Switch to Incidents view
|
||||
await pm.alertsPage.clickIncidentsTab();
|
||||
|
||||
// Type in incident search input
|
||||
const searchQuery = 'test';
|
||||
await pm.alertsPage.searchInIncidentsView(searchQuery);
|
||||
|
||||
// Verify search input has the value
|
||||
await expect(page.locator(pm.alertsPage.locators.incidentSearchInput)).toHaveValue(searchQuery);
|
||||
|
||||
// Verify incident table still visible (search results)
|
||||
await expect(page.locator(pm.alertsPage.locators.incidentListTable)).toBeVisible();
|
||||
|
||||
testLogger.info('[P1] ✓ Search in Incidents view verified');
|
||||
});
|
||||
|
||||
test("TC-09: 'All Folders' toggle only in Alerts view", {
|
||||
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P1] Testing All Folders toggle visibility');
|
||||
|
||||
// Verify toggle visible in Alerts view
|
||||
await expect(page.locator(pm.alertsPage.locators.searchAcrossFoldersToggle)).toBeVisible();
|
||||
testLogger.info('Toggle visible in Alerts view');
|
||||
|
||||
// Switch to Incidents view
|
||||
await pm.alertsPage.clickIncidentsTab();
|
||||
|
||||
// Verify toggle hidden in Incidents view
|
||||
await expect(page.locator(pm.alertsPage.locators.searchAcrossFoldersToggle)).not.toBeVisible();
|
||||
testLogger.info('Toggle hidden in Incidents view');
|
||||
|
||||
// Switch back to Alerts view
|
||||
await pm.alertsPage.clickAlertsTab();
|
||||
|
||||
// Verify toggle visible again
|
||||
await expect(page.locator(pm.alertsPage.locators.searchAcrossFoldersToggle)).toBeVisible();
|
||||
testLogger.info('Toggle visible again in Alerts view');
|
||||
|
||||
testLogger.info('[P1] ✓ All Folders toggle visibility verified');
|
||||
});
|
||||
|
||||
test("TC-10: Import and Add Alert buttons only in Alerts view", {
|
||||
tag: ['@alertsIncidentsTabs', '@functional', '@P1', '@all']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P1] Testing Import and Add Alert buttons visibility');
|
||||
|
||||
// Verify buttons visible in Alerts view
|
||||
await expect(page.locator(pm.alertsPage.locators.alertImportButton)).toBeVisible();
|
||||
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).toBeVisible();
|
||||
testLogger.info('Buttons visible in Alerts view');
|
||||
|
||||
// Switch to Incidents view
|
||||
await pm.alertsPage.clickIncidentsTab();
|
||||
|
||||
// Verify buttons hidden in Incidents view
|
||||
await expect(page.locator(pm.alertsPage.locators.alertImportButton)).not.toBeVisible();
|
||||
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).not.toBeVisible();
|
||||
testLogger.info('Buttons hidden in Incidents view');
|
||||
|
||||
// Switch back to Alerts view
|
||||
await pm.alertsPage.clickAlertsTab();
|
||||
|
||||
// Verify buttons visible again
|
||||
await expect(page.locator(pm.alertsPage.locators.alertImportButton)).toBeVisible();
|
||||
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).toBeVisible();
|
||||
testLogger.info('Buttons visible again in Alerts view');
|
||||
|
||||
testLogger.info('[P1] ✓ Import and Add Alert buttons visibility verified');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// P2 - EDGE CASE TESTS
|
||||
// ========================================
|
||||
|
||||
test("TC-11: Empty alerts list shows empty state", {
|
||||
tag: ['@alertsIncidentsTabs', '@edgeCase', '@P2', '@all']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P2] Testing empty alerts list handling');
|
||||
|
||||
// Even if no alerts, table container should be present
|
||||
// Note: This test assumes there might be alerts. If no alerts exist,
|
||||
// the empty state will be visible within the table container.
|
||||
await expect(page.locator('[data-test="alert-list-table"]')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify tabs still functional
|
||||
await pm.alertsPage.expectViewModeTabsVisible();
|
||||
|
||||
// Verify Add Alert button still visible (key action for empty state)
|
||||
await expect(page.locator(pm.alertsPage.locators.addAlertButton)).toBeVisible();
|
||||
|
||||
testLogger.info('[P2] ✓ Empty alerts list handled correctly');
|
||||
});
|
||||
|
||||
test("TC-12: Empty incidents list shows empty state", {
|
||||
tag: ['@alertsIncidentsTabs', '@edgeCase', '@P2', '@all']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P2] Testing empty incidents list handling');
|
||||
|
||||
// Switch to Incidents view
|
||||
await pm.alertsPage.clickIncidentsTab();
|
||||
|
||||
// Even if no incidents, table container should be present
|
||||
await expect(page.locator(pm.alertsPage.locators.incidentListTable)).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify tabs still functional - can switch back
|
||||
await pm.alertsPage.clickAlertsTab();
|
||||
await pm.alertsPage.expectAlertsViewElementsVisible();
|
||||
|
||||
testLogger.info('[P2] ✓ Empty incidents list handled correctly');
|
||||
});
|
||||
|
||||
test("TC-13: Page refresh preserves current view", {
|
||||
tag: ['@alertsIncidentsTabs', '@edgeCase', '@P2', '@all']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('[P2] Testing page refresh preserves view state');
|
||||
|
||||
// Get current URL for alerts page
|
||||
const alertsUrl = page.url();
|
||||
testLogger.info(`Current alerts URL: ${alertsUrl}`);
|
||||
|
||||
// Test 1: Switch to Incidents view
|
||||
await pm.alertsPage.clickIncidentsTab();
|
||||
await pm.alertsPage.expectIncidentsViewElementsVisible();
|
||||
testLogger.info('Switched to Incidents view');
|
||||
|
||||
// Get URL with view=incidents parameter
|
||||
const incidentsUrl = page.url();
|
||||
testLogger.info(`Incidents URL: ${incidentsUrl}`);
|
||||
|
||||
// Refresh page - this should preserve the view parameter
|
||||
await page.reload({ waitUntil: 'networkidle', timeout: 30000 });
|
||||
testLogger.info('Page refreshed from Incidents view');
|
||||
|
||||
// Wait for any loading overlays
|
||||
const loadingOverlay = page.locator('.fullscreen.bg-blue');
|
||||
if (await loadingOverlay.isVisible().catch(() => false)) {
|
||||
await loadingOverlay.waitFor({ state: 'hidden', timeout: 30000 }).catch(() => {});
|
||||
}
|
||||
|
||||
// Wait for page to stabilize
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Verify tabs are visible
|
||||
await pm.alertsPage.expectViewModeTabsVisible();
|
||||
testLogger.info('Tabs visible after refresh');
|
||||
|
||||
// Verify still in Incidents view (URL state preserved)
|
||||
await pm.alertsPage.expectIncidentsViewElementsVisible();
|
||||
await pm.alertsPage.expectAlertsOnlyElementsHidden();
|
||||
testLogger.info('✓ Incidents view preserved after refresh');
|
||||
|
||||
// Test 2: Switch back to Alerts and verify
|
||||
await pm.alertsPage.clickAlertsTab();
|
||||
await pm.alertsPage.expectAlertsViewElementsVisible();
|
||||
|
||||
testLogger.info('[P2] ✓ Page refresh preserves view state correctly');
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --base /web/",
|
||||
"devcloud": "NODE_ENV=devcloud vite --mode=devcloud",
|
||||
"build-alpha1": "run-p type-check build-only-alpha1",
|
||||
"build-cloud-prod": "run-p type-check build-only-cloud-prod",
|
||||
|
||||
@@ -134,9 +134,7 @@ async function mountAlertList() {
|
||||
emits: ["update:active-tab"],
|
||||
template:
|
||||
'<div class="app-tabs-stub">' +
|
||||
'<button class="tab-all" @click="$emit(\'update:active-tab\', \'all\')">All</button>' +
|
||||
'<button class="tab-scheduled" @click="$emit(\'update:active-tab\', \'scheduled\')">Scheduled</button>' +
|
||||
'<button class="tab-realtime" @click="$emit(\'update:active-tab\', \'realTime\')">RealTime</button>' +
|
||||
'<button v-for="tab in tabs" :key="tab.value" :class="`tab-${tab.value}`" @click="$emit(\'update:active-tab\', tab.value)">{{tab.label}}</button>' +
|
||||
'</div>',
|
||||
},
|
||||
SelectFolderDropDown: true,
|
||||
@@ -295,11 +293,12 @@ const waitData = async (wrapper: any) => {
|
||||
|
||||
// 1. Basic rendering and structure
|
||||
describe("AlertList - basic rendering", () => {
|
||||
it("renders the page container and title", async () => {
|
||||
it("renders the page container and view mode tabs", async () => {
|
||||
const wrapper = await mountAlertList();
|
||||
await waitData(wrapper);
|
||||
expect(wrapper.find('[data-test="alert-list-page"]').exists()).toBe(true);
|
||||
expect(wrapper.get('[data-test="alerts-list-title"]').text()).toBeTruthy();
|
||||
// Verify view mode tabs exist (Alerts/Incidents tabs)
|
||||
expect(wrapper.find('[data-test="alert-incident-view-tabs"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("renders search input and toggle", async () => {
|
||||
@@ -336,20 +335,26 @@ describe("AlertList - data fetching and columns", () => {
|
||||
const wrapper: any = await mountAlertList();
|
||||
await waitData(wrapper);
|
||||
|
||||
// Ensure we're in alerts view mode first
|
||||
wrapper.vm.viewMode = 'alerts';
|
||||
await flushPromises();
|
||||
|
||||
// default tab: all
|
||||
wrapper.vm.activeTab = 'all';
|
||||
await flushPromises();
|
||||
let names = wrapper.vm.columns.map((c: any) => c.name);
|
||||
expect(names).toContain("period");
|
||||
expect(names).toContain("frequency");
|
||||
|
||||
// switch to realTime
|
||||
await wrapper.find(".tab-realtime").trigger("click");
|
||||
wrapper.vm.activeTab = 'realTime';
|
||||
await flushPromises();
|
||||
names = wrapper.vm.columns.map((c: any) => c.name);
|
||||
expect(names).not.toContain("period");
|
||||
expect(names).not.toContain("frequency");
|
||||
|
||||
// switch to scheduled
|
||||
await wrapper.find(".tab-scheduled").trigger("click");
|
||||
wrapper.vm.activeTab = 'scheduled';
|
||||
await flushPromises();
|
||||
names = wrapper.vm.columns.map((c: any) => c.name);
|
||||
expect(names).toContain("period");
|
||||
@@ -381,7 +386,13 @@ describe("AlertList - filtering behaviors", () => {
|
||||
const wrapper: any = await mountAlertList();
|
||||
await waitData(wrapper);
|
||||
|
||||
await wrapper.find(".tab-scheduled").trigger("click");
|
||||
// Ensure we're in alerts view mode
|
||||
wrapper.vm.viewMode = 'alerts';
|
||||
await flushPromises();
|
||||
|
||||
// Set activeTab and trigger filtering
|
||||
wrapper.vm.activeTab = 'scheduled';
|
||||
wrapper.vm.filterAlertsByTab(true);
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.vm.filteredResults.every((r: any) => !r.is_real_time)).toBe(true);
|
||||
@@ -391,7 +402,13 @@ describe("AlertList - filtering behaviors", () => {
|
||||
const wrapper: any = await mountAlertList();
|
||||
await waitData(wrapper);
|
||||
|
||||
await wrapper.find(".tab-realtime").trigger("click");
|
||||
// Ensure we're in alerts view mode
|
||||
wrapper.vm.viewMode = 'alerts';
|
||||
await flushPromises();
|
||||
|
||||
// Set activeTab and trigger filtering
|
||||
wrapper.vm.activeTab = 'realTime';
|
||||
wrapper.vm.filterAlertsByTab(true);
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.vm.filteredResults.every((r: any) => r.is_real_time)).toBe(true);
|
||||
@@ -415,7 +432,13 @@ describe("AlertList - filtering behaviors", () => {
|
||||
const wrapper: any = await mountAlertList();
|
||||
await waitData(wrapper);
|
||||
|
||||
await wrapper.find(".tab-scheduled").trigger("click");
|
||||
// Ensure we're in alerts view mode
|
||||
wrapper.vm.viewMode = 'alerts';
|
||||
await flushPromises();
|
||||
|
||||
// Set activeTab and trigger filtering
|
||||
wrapper.vm.activeTab = 'scheduled';
|
||||
wrapper.vm.filterAlertsByTab(true);
|
||||
await flushPromises();
|
||||
|
||||
wrapper.vm.filterAlertsByQuery("Scheduled");
|
||||
@@ -662,8 +685,16 @@ describe("AlertList - additional validations", () => {
|
||||
it(`tab ${tab} shows only matching rows (${idx})`, async () => {
|
||||
const wrapper: any = await mountAlertList();
|
||||
await waitData(wrapper);
|
||||
await wrapper.find(`.tab-${tab === "all" ? "all" : tab.toLowerCase()}`).trigger("click");
|
||||
|
||||
// Ensure we're in alerts view mode
|
||||
wrapper.vm.viewMode = 'alerts';
|
||||
await flushPromises();
|
||||
|
||||
// Set activeTab and trigger filtering
|
||||
wrapper.vm.activeTab = tab;
|
||||
wrapper.vm.filterAlertsByTab(true);
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.vm.filteredResults.every((r: any) => predicate(r))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,38 +27,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
class="flex justify-between full-width tw:py-3 tw:px-4 items-center"
|
||||
>
|
||||
<div class="tw:flex tw:items-center tw:gap-4">
|
||||
<!-- Icon-only View Mode Tabs -->
|
||||
<div class="view-mode-tabs">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
:class="viewMode === 'alerts' ? 'active-tab' : 'inactive-tab'"
|
||||
class="view-tab-btn"
|
||||
@click="onViewModeChange('alerts')"
|
||||
data-test="alerts-view-btn"
|
||||
>
|
||||
<q-icon name="notifications_active" size="18px" />
|
||||
<q-tooltip>{{ t('alerts.header') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
:class="viewMode === 'incidents' ? 'active-tab' : 'inactive-tab'"
|
||||
class="view-tab-btn"
|
||||
@click="onViewModeChange('incidents')"
|
||||
data-test="incidents-view-btn"
|
||||
>
|
||||
<q-icon name="crisis_alert" size="18px" />
|
||||
<q-tooltip>{{ t('alerts.incidents.title') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<!-- Page Title -->
|
||||
<div
|
||||
class="page-title tw:font-[600] tw:text-[20px]"
|
||||
:data-test="viewMode === 'alerts' ? 'alerts-list-title' : 'incidents-list-title'"
|
||||
>
|
||||
{{ viewMode === 'alerts' ? t('alerts.header') : t('alerts.incidents.title') }}
|
||||
<!-- View Mode Tabs (Alerts / Incidents) -->
|
||||
<div class="view-mode-tabs-container">
|
||||
<app-tabs
|
||||
:tabs="viewTabs"
|
||||
v-model:active-tab="viewMode"
|
||||
@update:active-tab="onViewModeChange"
|
||||
data-test="alert-incident-view-tabs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex q-ml-auto tw:ps-2 items-center">
|
||||
@@ -1174,6 +1150,18 @@ export default defineComponent({
|
||||
// Incident search query
|
||||
const incidentSearchQuery = ref("");
|
||||
|
||||
// View mode tabs (Alerts / Incidents)
|
||||
const viewTabs = computed(() => [
|
||||
{
|
||||
label: t('alerts.header'),
|
||||
value: 'alerts',
|
||||
},
|
||||
{
|
||||
label: t('alerts.incidents.title'),
|
||||
value: 'incidents',
|
||||
}
|
||||
]);
|
||||
|
||||
// Tabs for alerts view only (removed incidents tab)
|
||||
const alertTabs = reactive([
|
||||
{
|
||||
@@ -2788,6 +2776,7 @@ export default defineComponent({
|
||||
tabs,
|
||||
alertTabs,
|
||||
viewMode,
|
||||
viewTabs,
|
||||
onViewModeChange,
|
||||
incidentSearchQuery,
|
||||
filterAlertsByTab,
|
||||
@@ -2812,75 +2801,29 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.view-mode-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
.view-mode-tabs-container {
|
||||
margin-right: 24px;
|
||||
|
||||
.view-tab-btn {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active-tab {
|
||||
background: var(--q-primary);
|
||||
color: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.q-icon {
|
||||
color: white;
|
||||
}
|
||||
// Customize app-tabs for view mode switching
|
||||
::v-deep .app-tabs {
|
||||
.q-tabs {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
&.inactive-tab {
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
.q-tab {
|
||||
padding: 0 20px;
|
||||
min-height: 36px;
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.q-icon {
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
&__icon {
|
||||
font-size: 18px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
.body--dark {
|
||||
.view-mode-tabs {
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
|
||||
.view-tab-btn {
|
||||
&.inactive-tab {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.q-icon {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-btn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
@@ -367,7 +367,7 @@ export const getPath = () => {
|
||||
const pos = window.location.pathname.indexOf("/web/");
|
||||
const path =
|
||||
window.location.origin == "http://localhost:8081"
|
||||
? "/"
|
||||
? pos > -1 ? "/web/" : "/"
|
||||
: pos > -1
|
||||
? window.location.pathname.slice(0, pos + 5)
|
||||
: "";
|
||||
|
||||
Reference in New Issue
Block a user