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
This commit is contained in:
@@ -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();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -179,6 +179,10 @@ export default defineConfig<PluginOptions>({
|
||||
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'),
|
||||
|
||||
@@ -121,11 +121,10 @@ export function SavedSearchItem({
|
||||
{/* Apply button (magnifying glass) */}
|
||||
<IconButton
|
||||
name="search"
|
||||
aria-label={t('alerting.saved-searches.apply-aria-label', 'Apply search "{{name}}"', {
|
||||
tooltip={t('alerting.saved-searches.apply-tooltip', 'Apply search "{{name}}"', {
|
||||
name: search.name,
|
||||
})}
|
||||
onClick={onApply}
|
||||
tooltip={t('alerting.saved-searches.apply-tooltip', 'Apply this search')}
|
||||
size="md"
|
||||
variant="secondary"
|
||||
disabled={isDisabled}
|
||||
|
||||
@@ -19,7 +19,7 @@ const ui = {
|
||||
saveInput: byPlaceholderText(/enter a name/i),
|
||||
// Action buttons
|
||||
cancelButton: byRole('button', { name: /cancel/i }),
|
||||
applyButtons: byRole('button', { name: /apply this search/i }),
|
||||
applyButtons: byRole('button', { name: /apply search/i }),
|
||||
actionMenuButtons: byRole('button', { name: /actions/i }),
|
||||
deleteButton: byRole('button', { name: /delete/i }),
|
||||
// Menu items (using byRole for proper accessibility testing)
|
||||
|
||||
@@ -2681,8 +2681,7 @@
|
||||
},
|
||||
"saved-searches": {
|
||||
"actions-aria-label": "Actions",
|
||||
"apply-aria-label": "Apply search \"{{name}}\"",
|
||||
"apply-tooltip": "Apply this search",
|
||||
"apply-tooltip": "Apply search \"{{name}}\"",
|
||||
"button-label": "Saved searches",
|
||||
"cancel": "Cancel",
|
||||
"default-indicator": "Default search",
|
||||
|
||||
Reference in New Issue
Block a user