From 0faab257b1c22ab0bbc9ca6a913bbf88c1ad6451 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 14 Jan 2026 16:41:53 +0100 Subject: [PATCH] Alerting: Add E2E test configuration and fix saved searches tests (#116203) * Alerting: Fix and stabilize saved searches E2E tests Stabilizes the saved searches E2E tests by ensuring correct feature toggles are enabled and improving the data cleanup logic. Previously, `clearSavedSearches` relied on clearing localStorage, which was insufficient as saved searches are persisted server-side via the UserStorage API. The cleanup now correctly invokes the UserStorage API. Changes: - Add `alerting` project to Playwright configuration with authentication - Enable `alertingListViewV2`, `alertingFilterV2`, and `alertingSavedSearches` toggles in tests - Update `clearSavedSearches` helper to use UserStorage API for reliable cleanup - Improve test selectors to use more robust `getByRole` and `getByText` queries - Update `SavedSearchItem` to merge `aria-label` into `tooltip` for consistency * Fix saved searchers unit tests --- .../alerting-suite/saved-searches.spec.ts | 73 ++++++++++++++----- playwright.config.ts | 4 + .../rule-list/filter/SavedSearchItem.tsx | 3 +- .../rule-list/filter/SavedSearches.test.tsx | 2 +- public/locales/en-US/grafana.json | 3 +- 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/e2e-playwright/alerting-suite/saved-searches.spec.ts b/e2e-playwright/alerting-suite/saved-searches.spec.ts index 28de805a5a1..b4f6431a56b 100644 --- a/e2e-playwright/alerting-suite/saved-searches.spec.ts +++ b/e2e-playwright/alerting-suite/saved-searches.spec.ts @@ -2,6 +2,15 @@ import { Page } from '@playwright/test'; import { test, expect } from '@grafana/plugin-e2e'; +// Enable required feature toggles for Saved Searches (part of RuleList.v2) +test.use({ + featureToggles: { + alertingListViewV2: true, + alertingFilterV2: true, + alertingSavedSearches: true, + }, +}); + /** * UI selectors for Saved Searches e2e tests. * Each selector is a function that takes the page and returns a locator. @@ -26,26 +35,50 @@ const ui = { // Indicators emptyState: (page: Page) => page.getByText(/no saved searches/i), - defaultIcon: (page: Page) => page.locator('[title="Default search"]'), + defaultIcon: (page: Page) => page.getByRole('img', { name: /default search/i }), duplicateError: (page: Page) => page.getByText(/already exists/i), }; /** - * Helper to clear saved searches storage. - * UserStorage uses localStorage as fallback, so we clear both potential keys. + * Helper to clear saved searches from UserStorage. + * UserStorage persists data server-side via k8s API, so we need to delete via API. */ async function clearSavedSearches(page: Page) { - await page.evaluate(() => { - // Clear localStorage keys that might contain saved searches - // UserStorage stores under 'grafana.userstorage.alerting' pattern - const keysToRemove = Object.keys(localStorage).filter( - (key) => key.includes('alerting') && (key.includes('savedSearches') || key.includes('userstorage')) - ); - keysToRemove.forEach((key) => localStorage.removeItem(key)); + // Get namespace and user info from Grafana config + const storageInfo = await page.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const bootData = (window as any).grafanaBootData; + const user = bootData?.user; + const userUID = user?.uid === '' || !user?.uid ? String(user?.id ?? 'anonymous') : user.uid; + const resourceName = `alerting:${userUID}`; + const namespace = bootData?.settings?.namespace || 'default'; - // Also clear session storage visited flag - const sessionKeysToRemove = Object.keys(sessionStorage).filter((key) => key.includes('alerting')); - sessionKeysToRemove.forEach((key) => sessionStorage.removeItem(key)); + return { namespace, resourceName }; + }); + + // Delete the UserStorage resource + try { + await page.request.delete( + `/apis/userstorage.grafana.app/v0alpha1/namespaces/${storageInfo.namespace}/user-storage/${storageInfo.resourceName}` + ); + } catch (error) { + // Ignore 404 errors (resource doesn't exist) + if (!(error && typeof error === 'object' && 'status' in error && error.status === 404)) { + console.warn('Failed to clear saved searches:', error); + } + } + + // Also clear localStorage as fallback storage + await page.evaluate(({ resourceName }) => { + // The UserStorage key pattern is always `{resourceName}:{key}` + // For saved searches, the key is 'savedSearches' + const key = `${resourceName}:savedSearches`; + window.localStorage.removeItem(key); + }, storageInfo); + + // Clear session storage visited flag + await page.evaluate(() => { + window.sessionStorage.removeItem('grafana.alerting.ruleList.visited'); }); } @@ -150,7 +183,7 @@ test.describe( await ui.saveButton(page).click(); - await ui.saveNameInput(page).fill('Apply Test'); + await ui.saveNameInput(page).fill('Firing Rules'); await ui.saveConfirmButton(page).click(); // Clear the search @@ -159,7 +192,7 @@ test.describe( // Apply the saved search await ui.savedSearchesButton(page).click(); - await page.getByRole('button', { name: /apply search.*apply test/i }).click(); + await page.getByRole('button', { name: /apply.*search.*firing rules/i }).click(); // Verify the search input is updated await expect(ui.searchInput(page)).toHaveValue('state:firing'); @@ -182,7 +215,7 @@ test.describe( await ui.renameMenuItem(page).click(); // Enter new name - const renameInput = page.getByDisplayValue('Original Name'); + const renameInput = page.getByRole('textbox', { name: /enter a name/i }); await renameInput.clear(); await renameInput.fill('Renamed Search'); await page.keyboard.press('Enter'); @@ -260,12 +293,12 @@ test.describe( await expect(ui.saveNameInput(page)).toBeVisible(); - // Press Escape to cancel + // Press Escape to cancel - this closes the entire dropdown await page.keyboard.press('Escape'); - // Verify we're back to list mode - await expect(ui.saveNameInput(page)).not.toBeVisible(); - await expect(ui.saveButton(page)).toBeVisible(); + // Verify the entire dialog is closed + await expect(ui.dropdown(page)).not.toBeVisible(); + await expect(ui.saveButton(page)).not.toBeVisible(); }); } ); diff --git a/playwright.config.ts b/playwright.config.ts index 27ec8b97f13..d90602f0a46 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -179,6 +179,10 @@ export default defineConfig({ name: 'cloud-plugins', testDir: path.join(testDirRoot, '/cloud-plugins-suite'), }), + withAuth({ + name: 'alerting', + testDir: path.join(testDirRoot, '/alerting-suite'), + }), withAuth({ name: 'dashboard-new-layouts', testDir: path.join(testDirRoot, '/dashboard-new-layouts'), diff --git a/public/app/features/alerting/unified/rule-list/filter/SavedSearchItem.tsx b/public/app/features/alerting/unified/rule-list/filter/SavedSearchItem.tsx index 0fb92622b2d..eb4af39ac12 100644 --- a/public/app/features/alerting/unified/rule-list/filter/SavedSearchItem.tsx +++ b/public/app/features/alerting/unified/rule-list/filter/SavedSearchItem.tsx @@ -121,11 +121,10 @@ export function SavedSearchItem({ {/* Apply button (magnifying glass) */}