Skip to content

Commit 6d7327a

Browse files
authored
chore: add tests for alerts (#1844)
## Summary Adds playwright tests for alert creation from saved searches and dashboards
1 parent d661c80 commit 6d7327a

5 files changed

Lines changed: 204 additions & 51 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Locator, Page } from '@playwright/test';
2+
3+
import { WebhookAlertModalComponent } from './WebhookAlertModalComponent';
4+
5+
/**
6+
* SearchPageAlertModalComponent - Encapsulates the alert creation modal
7+
* that appears on the saved search page (DBSearchPageAlertModal).
8+
*/
9+
export class SearchPageAlertModalComponent {
10+
private readonly modal: Locator;
11+
private readonly addNewWebhookButtonLocator: Locator;
12+
private readonly createAlertButtonLocator: Locator;
13+
readonly webhookAlertModal: WebhookAlertModalComponent;
14+
15+
constructor(page: Page) {
16+
this.modal = page.getByTestId('alerts-modal');
17+
this.addNewWebhookButtonLocator = page.getByTestId(
18+
'add-new-webhook-button',
19+
);
20+
this.createAlertButtonLocator = page.getByRole('button', {
21+
name: 'Create Alert',
22+
});
23+
this.webhookAlertModal = new WebhookAlertModalComponent(page);
24+
}
25+
26+
get addNewWebhookButton() {
27+
return this.addNewWebhookButtonLocator;
28+
}
29+
30+
/**
31+
* Add a new incoming webhook and wait for the webhook creation modal to
32+
* fully unmount. Uses `detached` state (not just `hidden`) because the
33+
* fixed-position overlay from the inner modal can linger after the root
34+
* loses its dimensions, blocking subsequent clicks in the outer modal.
35+
*/
36+
async addWebhookAndWait(
37+
serviceType: 'Slack' | 'incident.io' | 'Generic',
38+
name: string,
39+
url: string,
40+
) {
41+
await this.addNewWebhookButtonLocator.click();
42+
// Confirm the webhook form is open before filling it
43+
await this.webhookAlertModal.webhookNameInput.waitFor({ state: 'visible' });
44+
await this.webhookAlertModal.addWebhook(serviceType, name, url);
45+
// Wait for the webhook form to be fully removed from the DOM so the
46+
// fixed overlay is gone before we interact with the outer modal
47+
await this.webhookAlertModal.webhookNameInput.waitFor({
48+
state: 'detached',
49+
timeout: 10000,
50+
});
51+
}
52+
53+
/**
54+
* Submit the alert creation form and wait for the modal to close.
55+
*/
56+
async createAlert() {
57+
await this.createAlertButtonLocator.click();
58+
await this.modal.waitFor({ state: 'hidden' });
59+
}
60+
}

‎packages/app/tests/e2e/components/WebhookAlertModalComponent.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Locator, Page } from '@playwright/test';
33
export class WebhookAlertModalComponent {
44
private readonly modal: Locator;
55
private readonly serviceTypeRadioGroup: Locator;
6-
private readonly webhookNameInput: Locator;
6+
readonly webhookNameInput: Locator;
77
private readonly webhookUrlInput: Locator;
88
private readonly addWebhookButton: Locator;
99

Lines changed: 109 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,125 @@
11
import { AlertsPage } from '../page-objects/AlertsPage';
2+
import { DashboardPage } from '../page-objects/DashboardPage';
3+
import { SearchPage } from '../page-objects/SearchPage';
24
import { expect, test } from '../utils/base-test';
35

4-
test.skip('Alerts Functionality', { tag: ['@alerts', '@full-stack'] }, () => {
6+
test.describe('Alert Creation', { tag: ['@alerts', '@full-stack'] }, () => {
7+
let searchPage: SearchPage;
8+
let dashboardPage: DashboardPage;
59
let alertsPage: AlertsPage;
610

711
test.beforeEach(async ({ page }) => {
12+
searchPage = new SearchPage(page);
13+
dashboardPage = new DashboardPage(page);
814
alertsPage = new AlertsPage(page);
9-
await alertsPage.goto();
1015
});
1116

12-
test('should load alerts page', async () => {
13-
await test.step('Navigate to alerts page', async () => {
14-
await alertsPage.goto();
15-
});
17+
test(
18+
'should create an alert from a saved search and verify on the alerts page',
19+
{ tag: '@full-stack' },
20+
async () => {
21+
const ts = Date.now();
22+
const savedSearchName = `E2E Alert Search ${ts}`;
23+
const webhookName = `E2E Webhook SS ${ts}`;
24+
const webhookUrl = `https://example.com/ss-${ts}`;
1625

17-
await test.step('Verify alerts page loads with content', async () => {
18-
await expect(alertsPage.pageContainer).toBeVisible();
26+
await test.step('Create a saved search', async () => {
27+
await searchPage.goto();
28+
await searchPage.openSaveSearchModal();
29+
await searchPage.savedSearchModal.saveSearchAndWaitForNavigation(
30+
savedSearchName,
31+
);
32+
});
1933

20-
// Verify there are alert cards using web-first assertion
21-
const alertCards = alertsPage.getAlertCards();
22-
try {
23-
await expect(alertCards).toHaveCount(1, { timeout: 10000 });
24-
} catch {
25-
// If there are no alerts, just verify the container is visible
34+
await test.step('Open the alerts modal from the saved search page', async () => {
35+
await expect(searchPage.alertsButton).toBeVisible();
36+
await searchPage.openAlertsModal();
37+
await expect(searchPage.alertModal.addNewWebhookButton).toBeVisible();
38+
});
39+
40+
await test.step('Create a new incoming webhook for the alert channel', async () => {
41+
await searchPage.alertModal.addWebhookAndWait(
42+
'Generic',
43+
webhookName,
44+
webhookUrl,
45+
);
46+
});
47+
48+
await test.step('Create the alert (webhook is auto-selected after creation)', async () => {
49+
// The webhook is automatically selected in the form after webhook creation
50+
// (handleWebhookCreated calls field.onChange(webhookId) before closing modal)
51+
await searchPage.alertModal.createAlert();
52+
});
53+
54+
await test.step('Verify the alert is visible on the alerts page', async () => {
55+
await alertsPage.goto();
2656
await expect(alertsPage.pageContainer).toBeVisible();
27-
}
28-
});
29-
30-
await test.step('Verify alert links are accessible', async () => {
31-
const firstAlertLink = alertsPage.getAlertLink(0);
32-
// Only verify if alerts exist
33-
const isVisible = await firstAlertLink
34-
.isVisible({ timeout: 2000 })
35-
.catch(() => false);
36-
if (isVisible) {
37-
await expect(firstAlertLink).toBeVisible();
38-
}
39-
});
40-
});
57+
await expect(
58+
alertsPage.pageContainer
59+
.getByRole('link')
60+
.filter({ hasText: savedSearchName }),
61+
).toBeVisible({ timeout: 10000 });
62+
});
63+
},
64+
);
4165

42-
test('should handle alerts creation from search', async () => {
43-
await test.step('Navigate to search page', async () => {
44-
await alertsPage.page.goto('/search');
45-
});
66+
test(
67+
'should create an alert from a dashboard tile and verify on the alerts page',
68+
{ tag: '@full-stack' },
69+
async ({ page }) => {
70+
const ts = Date.now();
71+
const tileName = `E2E Alert Tile ${ts}`;
72+
const webhookName = `E2E Webhook Tile ${ts}`;
73+
const webhookUrl = `https://example.com/tile-${ts}`;
4674

47-
await test.step('Open alerts creation modal', async () => {
48-
await expect(alertsPage.createButton).toBeVisible();
49-
await alertsPage.openAlertsModal();
50-
});
75+
await test.step('Create a new dashboard', async () => {
76+
await dashboardPage.goto();
77+
await dashboardPage.createNewDashboard();
78+
});
5179

52-
await test.step('Verify alerts modal opens', async () => {
53-
await expect(alertsPage.modal).toBeVisible();
54-
});
55-
});
80+
await test.step('Add a tile to the dashboard', async () => {
81+
await dashboardPage.addTile();
82+
await expect(dashboardPage.chartEditor.nameInput).toBeVisible();
83+
await dashboardPage.chartEditor.waitForDataToLoad();
84+
await dashboardPage.chartEditor.setChartName(tileName);
85+
await dashboardPage.chartEditor.runQuery();
86+
});
87+
88+
await test.step('Enable and configure an alert on the tile', async () => {
89+
await expect(dashboardPage.chartEditor.alertButton).toBeVisible();
90+
await dashboardPage.chartEditor.clickAddAlert();
91+
await expect(
92+
dashboardPage.chartEditor.addNewWebhookButton,
93+
).toBeVisible();
94+
await dashboardPage.chartEditor.addNewWebhookButton.click();
95+
// Verify webhook form opened by checking for its inner input
96+
await expect(page.getByTestId('webhook-name-input')).toBeVisible();
97+
await dashboardPage.chartEditor.webhookAlertModal.addWebhook(
98+
'Generic',
99+
webhookName,
100+
webhookUrl,
101+
);
102+
await expect(page.getByTestId('alert-modal')).toBeHidden();
103+
// The webhook is automatically selected in the form after creation
104+
// (handleWebhookCreated calls field.onChange(webhookId) before closing modal)
105+
});
106+
107+
await test.step('Save the tile with the alert configured', async () => {
108+
await dashboardPage.chartEditor.save();
109+
await expect(dashboardPage.getTiles()).toHaveCount(1, {
110+
timeout: 10000,
111+
});
112+
});
113+
114+
await test.step('Verify the alert is visible on the alerts page', async () => {
115+
await alertsPage.goto();
116+
await expect(alertsPage.pageContainer).toBeVisible();
117+
await expect(
118+
alertsPage.pageContainer
119+
.getByRole('link')
120+
.filter({ hasText: tileName }),
121+
).toBeVisible({ timeout: 10000 });
122+
});
123+
},
124+
);
56125
});

‎packages/app/tests/e2e/features/dashboard.spec.ts‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,9 @@ test.describe('Dashboard', { tag: ['@dashboard'] }, () => {
287287

288288
test('should add and remove alert on Number type chart', async () => {
289289
test.setTimeout(60000);
290+
const ts = Date.now();
291+
const tileName = `E2E Alert Number Chart ${ts}`;
292+
const webhookUrl = `https://example.com/number-chart-${ts}`;
290293
const alertsPage = new AlertsPage(dashboardPage.page);
291294

292295
await test.step('Create new dashboard', async () => {
@@ -310,6 +313,8 @@ test.describe('Dashboard', { tag: ['@dashboard'] }, () => {
310313
'k8s.pod.cpu.utilization:::::::gauge',
311314
);
312315

316+
await dashboardPage.chartEditor.setChartName(tileName);
317+
313318
await expect(dashboardPage.chartEditor.alertButton).toHaveText(
314319
'Add Alert',
315320
);
@@ -318,8 +323,8 @@ test.describe('Dashboard', { tag: ['@dashboard'] }, () => {
318323

319324
await dashboardPage.chartEditor.webhookAlertModal.addWebhook(
320325
'Generic',
321-
'Test Webhook',
322-
'https://example.com/webhook',
326+
`Test Webhook ${ts}`,
327+
webhookUrl,
323328
);
324329

325330
await dashboardPage.saveTile();
@@ -357,10 +362,11 @@ test.describe('Dashboard', { tag: ['@dashboard'] }, () => {
357362

358363
await test.step('Verify alerts page loads with content', async () => {
359364
await expect(alertsPage.pageContainer).toBeVisible();
360-
361-
// Verify there are alert cards using web-first assertion
362-
const alertCards = alertsPage.getAlertCards();
363-
await expect(alertCards).toHaveCount(1, { timeout: 10000 });
365+
await expect(
366+
alertsPage.pageContainer
367+
.getByRole('link')
368+
.filter({ hasText: tileName }),
369+
).toBeVisible({ timeout: 10000 });
364370
});
365371

366372
await test.step('Navigate back to dashboard page', async () => {
@@ -385,10 +391,11 @@ test.describe('Dashboard', { tag: ['@dashboard'] }, () => {
385391

386392
await test.step('Verify alerts page loads with no alerts', async () => {
387393
await expect(alertsPage.pageContainer).toBeVisible();
388-
389-
// Verify there are alert cards using web-first assertion
390-
const alertCards = alertsPage.getAlertCards();
391-
await expect(alertCards).toHaveCount(0, { timeout: 10000 });
394+
await expect(
395+
alertsPage.pageContainer
396+
.getByRole('link')
397+
.filter({ hasText: tileName }),
398+
).toBeHidden({ timeout: 10000 });
392399
});
393400
});
394401

‎packages/app/tests/e2e/page-objects/SearchPage.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Locator, Page } from '@playwright/test';
77
import { FilterComponent } from '../components/FilterComponent';
88
import { InfrastructurePanelComponent } from '../components/InfrastructurePanelComponent';
99
import { SavedSearchModalComponent } from '../components/SavedSearchModalComponent';
10+
import { SearchPageAlertModalComponent } from '../components/SearchPageAlertModalComponent';
1011
import { SidePanelComponent } from '../components/SidePanelComponent';
1112
import { TableComponent } from '../components/TableComponent';
1213
import { TimePickerComponent } from '../components/TimePickerComponent';
@@ -22,9 +23,12 @@ export class SearchPage {
2223
readonly infrastructure: InfrastructurePanelComponent;
2324
readonly filters: FilterComponent;
2425
readonly savedSearchModal: SavedSearchModalComponent;
26+
readonly alertModal: SearchPageAlertModalComponent;
2527
readonly defaultTimeout: number = 3000;
2628
readonly editSourceMenuItem: Locator;
2729

30+
private readonly alertsButtonLocator: Locator;
31+
2832
// Page-specific locators
2933
private readonly searchForm: Locator;
3034
private readonly searchInput: Locator;
@@ -51,6 +55,8 @@ export class SearchPage {
5155
this.infrastructure = new InfrastructurePanelComponent(page);
5256
this.filters = new FilterComponent(page);
5357
this.savedSearchModal = new SavedSearchModalComponent(page);
58+
this.alertModal = new SearchPageAlertModalComponent(page);
59+
this.alertsButtonLocator = page.getByTestId('alerts-button');
5460

5561
// Define page-specific locators
5662
this.searchForm = page.getByTestId('search-form');
@@ -189,6 +195,17 @@ export class SearchPage {
189195
await button.click();
190196
}
191197

198+
/**
199+
* Open the alerts creation modal for the current saved search
200+
*/
201+
async openAlertsModal() {
202+
await this.alertsButtonLocator.click();
203+
}
204+
205+
get alertsButton() {
206+
return this.alertsButtonLocator;
207+
}
208+
192209
/**
193210
* Get search results table
194211
*/

0 commit comments

Comments
 (0)