Skip to content

Commit 807c7c1

Browse files
test[DI-28502]:- Notification channel Management - Listing Page (#13204)
* test[DI-28502]:- Notification channel Management - Listing Page * test[DI-28502]:- Notification channel Management - Listing Page * test[DI-28502]:- Remove hardcoded values from notification channel sorting * test[DI-28502]:- Remove hardcoded values from notification channel sorting * test[DI-28502]:- Remove hardcoded values from notification channel sorting
1 parent c3cbed8 commit 807c7c1

File tree

5 files changed

+369
-3
lines changed

5 files changed

+369
-3
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Tests
3+
---
4+
5+
Add coverage for the CloudPulse alerts notification channels listing ([#13204](https://github.com/linode/manager/pull/13204))
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
/**
2+
* @file Integration Tests for CloudPulse Alerting — Notification Channel Listing Page
3+
*/
4+
import { profileFactory } from '@linode/utilities';
5+
import { mockGetAccount } from 'support/intercepts/account';
6+
import { mockGetAlertChannels } from 'support/intercepts/cloudpulse';
7+
import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags';
8+
import { mockGetProfile } from 'support/intercepts/profile';
9+
import { ui } from 'support/ui';
10+
11+
import {
12+
accountFactory,
13+
flagsFactory,
14+
notificationChannelFactory,
15+
} from 'src/factories';
16+
import {
17+
ChannelAlertsTooltipText,
18+
ChannelListingTableLabelMap,
19+
} from 'src/features/CloudPulse/Alerts/NotificationChannels/NotificationsChannelsListing/constants';
20+
import { formatDate } from 'src/utilities/formatDate';
21+
22+
import type { NotificationChannel } from '@linode/api-v4';
23+
24+
const sortOrderMap = {
25+
ascending: 'asc',
26+
descending: 'desc',
27+
};
28+
29+
const LabelLookup = Object.fromEntries(
30+
ChannelListingTableLabelMap.map((item) => [item.colName, item.label])
31+
);
32+
type SortOrder = 'ascending' | 'descending';
33+
34+
interface VerifyChannelSortingParams {
35+
columnLabel: string;
36+
expected: number[];
37+
sortOrder: SortOrder;
38+
}
39+
40+
const notificationChannels = notificationChannelFactory
41+
.buildList(26)
42+
.map((ch, i) => {
43+
const isEmail = i % 2 === 0;
44+
const alerts = Array.from({ length: isEmail ? 5 : 3 }).map((_, idx) => ({
45+
id: idx + 1,
46+
label: `Alert-${idx + 1}`,
47+
type: 'alerts-definitions',
48+
url: 'Sample',
49+
}));
50+
51+
if (isEmail) {
52+
return {
53+
...ch,
54+
id: i + 1,
55+
label: `Channel-${i + 1}`,
56+
type: 'custom',
57+
created_by: 'user',
58+
updated_by: 'user',
59+
channel_type: 'email',
60+
updated: new Date(2024, 0, i + 1).toISOString(),
61+
alerts,
62+
content: {
63+
email: {
64+
email_addresses: [`test-${i + 1}@example.com`],
65+
subject: 'Test Subject',
66+
message: 'Test message',
67+
},
68+
},
69+
} as NotificationChannel;
70+
} else {
71+
return {
72+
...ch,
73+
id: i + 1,
74+
label: `Channel-${i + 1}`,
75+
type: 'default',
76+
created_by: 'system',
77+
updated_by: 'system',
78+
channel_type: 'webhook',
79+
updated: new Date(2024, 0, i + 1).toISOString(),
80+
alerts,
81+
content: {
82+
webhook: {
83+
webhook_url: `https://example.com/webhook/${i + 1}`,
84+
http_headers: [
85+
{
86+
header_key: 'Authorization',
87+
header_value: 'Bearer secret-token',
88+
},
89+
],
90+
},
91+
},
92+
} as NotificationChannel;
93+
}
94+
});
95+
96+
const isEmailContent = (
97+
content: NotificationChannel['content']
98+
): content is {
99+
email: {
100+
email_addresses: string[];
101+
message: string;
102+
subject: string;
103+
};
104+
} => 'email' in content;
105+
const mockProfile = profileFactory.build({
106+
timezone: 'gmt',
107+
});
108+
109+
/**
110+
* Verifies sorting of a column in the alerts table.
111+
*
112+
* @param params - Configuration object for sorting verification.
113+
* @param params.columnLabel - The label of the column to sort.
114+
* @param params.sortOrder - Expected sorting order (ascending | descending).
115+
* @param params.expected - Expected row order after sorting.
116+
*/
117+
const VerifyChannelSortingParams = (
118+
columnLabel: string,
119+
sortOrder: 'ascending' | 'descending',
120+
expected: number[]
121+
) => {
122+
cy.get(`[data-qa-header="${columnLabel}"]`).click({ force: true });
123+
124+
cy.get(`[data-qa-header="${columnLabel}"]`)
125+
.invoke('attr', 'aria-sort')
126+
.then((current) => {
127+
if (current !== sortOrder) {
128+
cy.get(`[data-qa-header="${columnLabel}"]`).click({ force: true });
129+
}
130+
});
131+
132+
cy.get(`[data-qa-header="${columnLabel}"]`).should(
133+
'have.attr',
134+
'aria-sort',
135+
sortOrder
136+
);
137+
138+
cy.get('[data-qa="notification-channels-table"] tbody:last-of-type tr').then(
139+
($rows) => {
140+
const actualOrder = $rows
141+
.toArray()
142+
.map((row) =>
143+
Number(row.getAttribute('data-qa-notification-channel-cell'))
144+
);
145+
expect(actualOrder).to.eqls(expected);
146+
}
147+
);
148+
149+
const order = sortOrderMap[sortOrder];
150+
const orderBy = LabelLookup[columnLabel];
151+
152+
cy.url().should(
153+
'endWith',
154+
`/alerts/notification-channels?order=${order}&orderBy=${orderBy}`
155+
);
156+
};
157+
158+
describe('Notification Channel Listing Page', () => {
159+
/**
160+
* Validates the listing page for CloudPulse notification channels.
161+
* Confirms channel data rendering, search behavior, and table sorting
162+
* across all columns using a controlled 26-item mock dataset.
163+
*/
164+
beforeEach(() => {
165+
mockAppendFeatureFlags(flagsFactory.build());
166+
mockGetProfile(mockProfile);
167+
mockGetAccount(accountFactory.build());
168+
mockGetAlertChannels(notificationChannels).as(
169+
'getAlertNotificationChannels'
170+
);
171+
172+
cy.visitWithLogin('/alerts/notification-channels');
173+
174+
ui.pagination.findPageSizeSelect().click();
175+
176+
cy.get('[data-qa-pagination-page-size-option="100"]')
177+
.should('exist')
178+
.click();
179+
180+
ui.tooltip.findByText(ChannelAlertsTooltipText).should('be.visible');
181+
182+
cy.wait('@getAlertNotificationChannels').then(({ response }) => {
183+
const body = response?.body;
184+
const data = body?.data;
185+
186+
const channels = data as NotificationChannel[];
187+
188+
expect(body?.results).to.eq(notificationChannels.length);
189+
190+
channels.forEach((item, index) => {
191+
const expected = notificationChannels[index];
192+
193+
// Basic fields
194+
expect(item.id).to.eq(expected.id);
195+
expect(item.label).to.eq(expected.label);
196+
expect(item.type).to.eq(expected.type);
197+
expect(item.status).to.eq(expected.status);
198+
expect(item.channel_type).to.eq(expected.channel_type);
199+
200+
// Creator/updater fields
201+
expect(item.created_by).to.eq(expected.created_by);
202+
expect(item.updated_by).to.eq(expected.updated_by);
203+
204+
// Email content (safe narrow)
205+
if (isEmailContent(item.content) && isEmailContent(expected.content)) {
206+
expect(item.content.email.email_addresses).to.deep.eq(
207+
expected.content.email.email_addresses
208+
);
209+
expect(item.content.email.subject).to.eq(
210+
expected.content.email.subject
211+
);
212+
expect(item.content.email.message).to.eq(
213+
expected.content.email.message
214+
);
215+
}
216+
217+
// Alerts list
218+
expect(item.alerts.length).to.eq(expected.alerts.length);
219+
220+
item.alerts.forEach((alert, aIndex) => {
221+
const expAlert = expected.alerts[aIndex];
222+
223+
expect(alert.id).to.eq(expAlert.id);
224+
expect(alert.label).to.eq(expAlert.label);
225+
expect(alert.type).to.eq(expAlert.type);
226+
expect(alert.url).to.eq(expAlert.url);
227+
});
228+
});
229+
});
230+
});
231+
232+
it('searches and validates notification channel details', () => {
233+
cy.findByPlaceholderText('Search for Notification Channels').as(
234+
'searchInput'
235+
);
236+
237+
cy.get('[data-qa="notification-channels-table"]')
238+
.find('tbody')
239+
.last()
240+
.within(() => {
241+
cy.get('tr').should('have.length', 26);
242+
});
243+
244+
cy.get('@searchInput').clear();
245+
cy.get('@searchInput').type('Channel-9');
246+
cy.get('[data-qa="notification-channels-table"]')
247+
.find('tbody')
248+
.last()
249+
.within(() => {
250+
cy.get('tr').should('have.length', 1);
251+
252+
cy.get('tr').each(($row) => {
253+
const expected = notificationChannels[8];
254+
255+
cy.wrap($row).within(() => {
256+
cy.findByText(expected.label).should('be.visible');
257+
cy.findByText(String(expected.alerts.length)).should('be.visible');
258+
cy.findByText('Email').should('be.visible');
259+
cy.get('td').eq(3).should('have.text', expected.created_by);
260+
cy.findByText(
261+
formatDate(expected.updated, {
262+
format: 'MMM dd, yyyy, h:mm a',
263+
timezone: 'GMT',
264+
})
265+
).should('be.visible');
266+
cy.get('td').eq(5).should('have.text', expected.updated_by);
267+
});
268+
});
269+
});
270+
});
271+
272+
it('sorting and validates notification channel details', () => {
273+
const sortColumns = [
274+
{
275+
column: 'Channel Name',
276+
ascending: [...notificationChannels]
277+
.sort((a, b) => a.label.localeCompare(b.label))
278+
.map((ch) => ch.id),
279+
280+
descending: [...notificationChannels]
281+
.sort((a, b) => b.label.localeCompare(a.label))
282+
.map((ch) => ch.id),
283+
},
284+
{
285+
column: 'Alerts',
286+
ascending: [...notificationChannels]
287+
.sort((a, b) => a.alerts.length - b.alerts.length)
288+
.map((ch) => ch.id),
289+
290+
descending: [...notificationChannels]
291+
.sort((a, b) => b.alerts.length - a.alerts.length)
292+
.map((ch) => ch.id),
293+
},
294+
295+
{
296+
column: 'Channel Type',
297+
ascending: [...notificationChannels]
298+
.sort((a, b) => a.channel_type.localeCompare(b.channel_type))
299+
.map((ch) => ch.id),
300+
301+
descending: [...notificationChannels]
302+
.sort((a, b) => b.channel_type.localeCompare(a.channel_type))
303+
.map((ch) => ch.id),
304+
},
305+
306+
{
307+
column: 'Created By',
308+
ascending: [...notificationChannels]
309+
.sort((a, b) => a.created_by.localeCompare(b.created_by))
310+
.map((ch) => ch.id),
311+
312+
descending: [...notificationChannels]
313+
.sort((a, b) => b.created_by.localeCompare(a.created_by))
314+
.map((ch) => ch.id),
315+
},
316+
{
317+
column: 'Last Modified',
318+
ascending: [...notificationChannels]
319+
.sort((a, b) => a.updated.localeCompare(b.updated))
320+
.map((ch) => ch.id),
321+
322+
descending: [...notificationChannels]
323+
.sort((a, b) => b.updated.localeCompare(a.updated))
324+
.map((ch) => ch.id),
325+
},
326+
{
327+
column: 'Last Modified By',
328+
ascending: [...notificationChannels]
329+
.sort((a, b) => a.updated_by.localeCompare(b.updated_by))
330+
.map((ch) => ch.id),
331+
332+
descending: [...notificationChannels]
333+
.sort((a, b) => b.updated_by.localeCompare(a.updated_by))
334+
.map((ch) => ch.id),
335+
},
336+
];
337+
338+
cy.get('[data-qa="notification-channels-table"] thead th').as('headers');
339+
340+
cy.get('@headers').then(($headers) => {
341+
const actual = Array.from($headers)
342+
.map((th) => th.textContent?.trim())
343+
.filter(Boolean);
344+
345+
expect(actual).to.deep.equal([
346+
'Channel Name',
347+
'Alerts',
348+
'Channel Type',
349+
'Created By',
350+
'Last Modified',
351+
'Last Modified By',
352+
]);
353+
});
354+
355+
sortColumns.forEach(({ column, ascending, descending }) => {
356+
VerifyChannelSortingParams(column, 'ascending', ascending);
357+
VerifyChannelSortingParams(column, 'descending', descending);
358+
});
359+
});
360+
});

packages/manager/cypress/e2e/core/cloudpulse/alerting-notification-channel-permission-tests.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @file Integration Tests for CloudPulse Alerting — Notification Channel Listing Page
33
*
4-
* Covers three access-control behaviors:
4+
* Covers four access-control behaviors:
55
* 1. Access is allowed when `notificationChannels` is true.
66
* 2. Navigation/tab visibility is blocked when `notificationChannels` is false.
77
* 3. Direct URL access is blocked when `notificationChannels` is false.
@@ -91,7 +91,6 @@ describe('Notification Channel Listing Page — Access Control', () => {
9191
it('blocks direct URL access to /alerts/notification-channels when notificationChannels is disabled', () => {
9292
const flags: Partial<Flags> = {
9393
aclp: { beta: true, enabled: true },
94-
9594
aclpAlerting: {
9695
accountAlertLimit: 10,
9796
accountMetricLimit: 10,

packages/manager/src/factories/featureFlags.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const flagsFactory = Factory.Sync.makeFactory<Partial<Flags>>({
2424
alertDefinitions: true,
2525
beta: true,
2626
recentActivity: false,
27-
notificationChannels: false,
27+
notificationChannels: true,
2828
editDisabledStatuses: [
2929
'in progress',
3030
'failed',

0 commit comments

Comments
 (0)