* 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
305 lines
10 KiB
TypeScript
305 lines
10 KiB
TypeScript
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.
|
|
*/
|
|
const ui = {
|
|
// Main elements
|
|
savedSearchesButton: (page: Page) => page.getByRole('button', { name: /saved searches/i }),
|
|
dropdown: (page: Page) => page.getByRole('dialog', { name: /saved searches/i }),
|
|
searchInput: (page: Page) => page.getByTestId('search-query-input'),
|
|
|
|
// Save functionality
|
|
saveButton: (page: Page) => page.getByRole('button', { name: /save current search/i }),
|
|
saveConfirmButton: (page: Page) => page.getByRole('button', { name: /^save$/i }),
|
|
saveNameInput: (page: Page) => page.getByPlaceholder(/enter a name/i),
|
|
|
|
// Action menu
|
|
actionsButton: (page: Page) => page.getByRole('button', { name: /actions/i }),
|
|
renameMenuItem: (page: Page) => page.getByText(/rename/i),
|
|
deleteMenuItem: (page: Page) => page.getByText(/^delete$/i),
|
|
setAsDefaultMenuItem: (page: Page) => page.getByText(/set as default/i),
|
|
deleteConfirmButton: (page: Page) => page.getByRole('button', { name: /^delete$/i }),
|
|
|
|
// Indicators
|
|
emptyState: (page: Page) => page.getByText(/no saved searches/i),
|
|
defaultIcon: (page: Page) => page.getByRole('img', { name: /default search/i }),
|
|
duplicateError: (page: Page) => page.getByText(/already exists/i),
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
// 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';
|
|
|
|
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');
|
|
});
|
|
}
|
|
|
|
test.describe(
|
|
'Alert Rules - Saved Searches',
|
|
{
|
|
tag: ['@alerting'],
|
|
},
|
|
() => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Clear any saved searches from previous tests before navigating
|
|
await page.goto('/alerting/list');
|
|
await clearSavedSearches(page);
|
|
await page.reload();
|
|
});
|
|
|
|
test.afterEach(async ({ page }) => {
|
|
// Clean up saved searches after each test
|
|
await clearSavedSearches(page);
|
|
});
|
|
|
|
test('should display Saved searches button', async ({ page }) => {
|
|
await expect(ui.savedSearchesButton(page)).toBeVisible();
|
|
});
|
|
|
|
test('should open dropdown when clicking Saved searches button', async ({ page }) => {
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
await expect(ui.dropdown(page)).toBeVisible();
|
|
});
|
|
|
|
test('should show empty state when no saved searches exist', async ({ page }) => {
|
|
// Storage is cleared in beforeEach, so we should see empty state
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
await expect(ui.emptyState(page)).toBeVisible();
|
|
});
|
|
|
|
test('should enable Save current search button when search query is entered', async ({ page }) => {
|
|
// Enter a search query
|
|
await ui.searchInput(page).fill('state:firing');
|
|
await ui.searchInput(page).press('Enter');
|
|
|
|
// Open saved searches
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
await expect(ui.saveButton(page)).toBeEnabled();
|
|
});
|
|
|
|
test('should disable Save current search button when search query is empty', async ({ page }) => {
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
await expect(ui.saveButton(page)).toBeDisabled();
|
|
});
|
|
|
|
test('should save a new search', async ({ page }) => {
|
|
// Enter a search query
|
|
await ui.searchInput(page).fill('state:firing');
|
|
await ui.searchInput(page).press('Enter');
|
|
|
|
// Open saved searches
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
// Click save button
|
|
await ui.saveButton(page).click();
|
|
|
|
// Enter name and save
|
|
await ui.saveNameInput(page).fill('My Firing Rules');
|
|
await ui.saveConfirmButton(page).click();
|
|
|
|
// Verify the saved search appears in the list
|
|
await expect(page.getByText('My Firing Rules')).toBeVisible();
|
|
});
|
|
|
|
test('should show validation error for duplicate name', async ({ page }) => {
|
|
// First save a search
|
|
await ui.searchInput(page).fill('state:firing');
|
|
await ui.searchInput(page).press('Enter');
|
|
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
await ui.saveButton(page).click();
|
|
|
|
await ui.saveNameInput(page).fill('Duplicate Test');
|
|
await ui.saveConfirmButton(page).click();
|
|
|
|
// Try to save another with the same name
|
|
await ui.saveButton(page).click();
|
|
await ui.saveNameInput(page).fill('Duplicate Test');
|
|
await ui.saveConfirmButton(page).click();
|
|
|
|
// Verify validation error
|
|
await expect(ui.duplicateError(page)).toBeVisible();
|
|
});
|
|
|
|
test('should apply a saved search', async ({ page }) => {
|
|
// Create a saved search first
|
|
await ui.searchInput(page).fill('state:firing');
|
|
await ui.searchInput(page).press('Enter');
|
|
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
await ui.saveButton(page).click();
|
|
|
|
await ui.saveNameInput(page).fill('Firing Rules');
|
|
await ui.saveConfirmButton(page).click();
|
|
|
|
// Clear the search
|
|
await ui.searchInput(page).clear();
|
|
await ui.searchInput(page).press('Enter');
|
|
|
|
// Apply the saved search
|
|
await ui.savedSearchesButton(page).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');
|
|
});
|
|
|
|
test('should rename a saved search', async ({ page }) => {
|
|
// Create a saved search
|
|
await ui.searchInput(page).fill('state:firing');
|
|
await ui.searchInput(page).press('Enter');
|
|
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
await ui.saveButton(page).click();
|
|
|
|
await ui.saveNameInput(page).fill('Original Name');
|
|
await ui.saveConfirmButton(page).click();
|
|
|
|
// Open action menu and click rename
|
|
await ui.actionsButton(page).click();
|
|
await ui.renameMenuItem(page).click();
|
|
|
|
// Enter new name
|
|
const renameInput = page.getByRole('textbox', { name: /enter a name/i });
|
|
await renameInput.clear();
|
|
await renameInput.fill('Renamed Search');
|
|
await page.keyboard.press('Enter');
|
|
|
|
// Verify the name was updated
|
|
await expect(page.getByText('Renamed Search')).toBeVisible();
|
|
await expect(page.getByText('Original Name')).not.toBeVisible();
|
|
});
|
|
|
|
test('should delete a saved search', async ({ page }) => {
|
|
// Create a saved search
|
|
await ui.searchInput(page).fill('state:firing');
|
|
await ui.searchInput(page).press('Enter');
|
|
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
await ui.saveButton(page).click();
|
|
|
|
await ui.saveNameInput(page).fill('To Delete');
|
|
await ui.saveConfirmButton(page).click();
|
|
|
|
// Verify it was saved
|
|
await expect(page.getByText('To Delete')).toBeVisible();
|
|
|
|
// Open action menu and click delete
|
|
await ui.actionsButton(page).click();
|
|
await ui.deleteMenuItem(page).click();
|
|
|
|
// Confirm delete
|
|
await ui.deleteConfirmButton(page).click();
|
|
|
|
// Verify it was deleted
|
|
await expect(page.getByText('To Delete')).not.toBeVisible();
|
|
});
|
|
|
|
test('should set a search as default', async ({ page }) => {
|
|
// Create a saved search
|
|
await ui.searchInput(page).fill('state:firing');
|
|
await ui.searchInput(page).press('Enter');
|
|
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
await ui.saveButton(page).click();
|
|
|
|
await ui.saveNameInput(page).fill('Default Test');
|
|
await ui.saveConfirmButton(page).click();
|
|
|
|
// Set as default
|
|
await ui.actionsButton(page).click();
|
|
await ui.setAsDefaultMenuItem(page).click();
|
|
|
|
// Verify the star icon appears (indicating default)
|
|
await expect(ui.defaultIcon(page)).toBeVisible();
|
|
});
|
|
|
|
test('should close dropdown when pressing Escape', async ({ page }) => {
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
await expect(ui.dropdown(page)).toBeVisible();
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
await expect(ui.dropdown(page)).not.toBeVisible();
|
|
});
|
|
|
|
test('should cancel save mode when pressing Escape', async ({ page }) => {
|
|
// Enter a search query
|
|
await ui.searchInput(page).fill('state:firing');
|
|
await ui.searchInput(page).press('Enter');
|
|
|
|
await ui.savedSearchesButton(page).click();
|
|
|
|
// Start save mode
|
|
await ui.saveButton(page).click();
|
|
|
|
await expect(ui.saveNameInput(page)).toBeVisible();
|
|
|
|
// Press Escape to cancel - this closes the entire dropdown
|
|
await page.keyboard.press('Escape');
|
|
|
|
// Verify the entire dialog is closed
|
|
await expect(ui.dropdown(page)).not.toBeVisible();
|
|
await expect(ui.saveButton(page)).not.toBeVisible();
|
|
});
|
|
}
|
|
);
|