Aniket Katkar fc21818a14 Fix the AUT flakiness in NotificationAlert and ServiceIngestion specs (#23489)
(cherry picked from commit 2ec118b4db1e8fc67b9653f3a30b44c84a888f12)
2025-09-21 14:40:16 +00:00

742 lines
18 KiB
TypeScript

/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, Page } from '@playwright/test';
import { ALERT_UPDATED_DESCRIPTION } from '../constant/alert';
import {
AlertDetails,
ObservabilityCreationDetails,
} from '../constant/alert.interface';
import { SidebarItem } from '../constant/sidebar';
import { Domain } from '../support/domain/Domain';
import { EntityTypeEndpoint } from '../support/entity/Entity.interface';
import { PipelineClass } from '../support/entity/PipelineClass';
import { TableClass } from '../support/entity/TableClass';
import { UserClass } from '../support/user/UserClass';
import {
addDomainFilter,
addEntityFQNFilter,
addOwnerFilter,
addPipelineStatusUpdatesAction,
checkRecentEventDetails,
inputBasicAlertInformation,
visitAlertDetailsPage,
visitEditAlertPage,
waitForRecentEventsToFinishExecution,
} from './alert';
import { clickOutside, descriptionBox, redirectToHomePage } from './common';
import { addMultiOwner, updateDescription } from './entity';
import { addInternalDestination } from './notificationAlert';
import { sidebarClick } from './sidebar';
export const visitObservabilityAlertPage = async (page: Page) => {
await redirectToHomePage(page);
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
// Set up the response promise before navigation
const getAlerts = page.waitForResponse(
'/api/v1/events/subscriptions?*alertType=Observability*'
);
// Set up navigation promise before clicking
const navigationPromise = page.waitForURL('**/observability/alerts', {
waitUntil: 'networkidle',
});
await sidebarClick(page, SidebarItem.OBSERVABILITY_ALERT);
// Wait for both navigation and API response
await Promise.all([navigationPromise, getAlerts]);
};
export const addExternalDestination = async ({
page,
destinationNumber,
category,
secretKey,
input = '',
}: {
page: Page;
destinationNumber: number;
category: string;
input?: string;
secretKey?: string;
}) => {
// Select destination category
await page.click(
`[data-testid="destination-category-select-${destinationNumber}"]`
);
await page.waitForSelector(`.ant-select-dropdown:visible`, {
state: 'visible',
});
// Select external tab
await page.click(`[data-testid="tab-label-external"]:visible`);
// Select destination category option
await page.click(
`[data-testid="destination-category-dropdown-${destinationNumber}"]:visible [data-testid="${category}-external-option"]:visible`
);
// Input the destination receivers value
if (category === 'Email') {
await page.fill(
`[data-testid="email-input-${destinationNumber}"] [role="combobox"]`,
input
);
await page.keyboard.press('Enter');
} else {
await page.fill(
`[data-testid="endpoint-input-${destinationNumber}"]`,
input
);
}
// Input the secret key value
if (category === 'Webhook' && secretKey) {
await page
.getByTestId(`destination-${destinationNumber}`)
.getByText('Advanced Configuration')
.click();
await expect(
page.getByTestId(`secret-key-input-${destinationNumber}`)
).toBeVisible();
await page.fill(
`[data-testid="secret-key-input-${destinationNumber}"]`,
secretKey
);
}
await clickOutside(page);
};
export const getObservabilityCreationDetails = ({
tableName1,
tableName2,
testCaseName,
ingestionPipelineName,
domainName,
domainDisplayName,
userName,
testSuiteFQN,
}: {
tableName1: string;
tableName2: string;
testCaseName: string;
ingestionPipelineName: string;
domainName: string;
domainDisplayName: string;
userName: string;
testSuiteFQN: string;
}): Array<ObservabilityCreationDetails> => {
return [
{
source: 'table',
sourceDisplayName: 'Table',
filters: [
{
name: 'Table Name',
inputSelector: 'fqn-list-select',
inputValue: tableName1,
exclude: true,
},
{
name: 'Domain',
inputSelector: 'domain-select',
inputValue: domainName,
inputValueId: domainDisplayName,
exclude: false,
},
{
name: 'Owner Name',
inputSelector: 'owner-name-select',
inputValue: userName,
exclude: true,
},
],
actions: [
{
name: 'Get Schema Changes',
exclude: true,
},
{
name: 'Get Table Metrics Updates',
exclude: false,
},
],
destinations: [
{
mode: 'internal',
category: 'Owners',
type: 'Email',
},
{
mode: 'external',
category: 'Webhook',
inputValue: 'https://webhook.com',
secretKey: 'secret_key',
},
],
},
{
source: 'ingestionPipeline',
sourceDisplayName: 'Ingestion Pipeline',
filters: [
{
name: 'Ingestion Pipeline Name',
inputSelector: 'fqn-list-select',
inputValue: ingestionPipelineName,
exclude: false,
},
{
name: 'Domain',
inputSelector: 'domain-select',
inputValue: domainName,
inputValueId: domainDisplayName,
exclude: false,
},
{
name: 'Owner Name',
inputSelector: 'owner-name-select',
inputValue: userName,
exclude: true,
},
],
actions: [
{
name: 'Get Ingestion Pipeline Status Updates',
inputs: [
{
inputSelector: 'pipeline-status-select',
inputValue: 'Queued',
},
],
exclude: false,
},
],
destinations: [
{
mode: 'internal',
category: 'Owners',
type: 'Email',
},
{
mode: 'external',
category: 'Email',
inputValue: 'test@example.com',
},
],
},
{
source: 'testCase',
sourceDisplayName: 'Test case',
filters: [
{
name: 'Test Case Name',
inputSelector: 'fqn-list-select',
inputValue: testCaseName,
exclude: true,
},
{
name: 'Domain',
inputSelector: 'domain-select',
inputValue: domainName,
inputValueId: domainDisplayName,
exclude: false,
},
{
name: 'Owner Name',
inputSelector: 'owner-name-select',
inputValue: userName,
exclude: true,
},
{
name: 'Table Name A Test Case Belongs To',
inputSelector: 'table-name-select',
inputValue: tableName2,
exclude: false,
},
],
actions: [
{
name: 'Get Test Case Status Updates',
inputs: [
{
inputSelector: 'test-result-select',
inputValue: 'Success',
},
],
exclude: false,
},
{
name: 'Get Test Case Status Updates belonging to a Test Suite',
inputs: [
{
inputSelector: 'test-suite-select',
inputValue: testSuiteFQN,
waitForAPI: true,
},
{
inputSelector: 'test-status-select',
inputValue: 'Failed',
},
],
exclude: false,
},
],
destinations: [
{
mode: 'internal',
category: 'Users',
inputSelector: 'User-select',
inputValue: userName,
type: 'Email',
},
{
mode: 'external',
category: 'Webhook',
inputValue: 'https://webhook.com',
},
],
},
{
source: 'testSuite',
sourceDisplayName: 'Test Suite',
filters: [
{
name: 'Test Suite Name',
inputSelector: 'fqn-list-select',
inputValue: testSuiteFQN,
exclude: true,
},
{
name: 'Domain',
inputSelector: 'domain-select',
inputValue: domainName,
inputValueId: domainDisplayName,
exclude: false,
},
{
name: 'Owner Name',
inputSelector: 'owner-name-select',
inputValue: userName,
exclude: false,
},
],
actions: [
{
name: 'Get Test Suite Status Updates',
inputs: [
{
inputSelector: 'test-result-select',
inputValue: 'Failed',
},
],
exclude: false,
},
],
destinations: [
{
mode: 'external',
category: 'Slack',
inputValue: 'https://slack.com',
},
],
},
];
};
export const editObservabilityAlert = async ({
page,
alertDetails,
sourceName,
sourceDisplayName,
user,
domain,
pipeline,
}: {
page: Page;
alertDetails: AlertDetails;
sourceName: string;
sourceDisplayName: string;
user: UserClass;
domain: Domain;
pipeline: PipelineClass;
}) => {
await visitEditAlertPage(page, alertDetails, false);
// Update description
await page.locator(descriptionBox).clear();
await page.locator(descriptionBox).fill(ALERT_UPDATED_DESCRIPTION);
// Update source
await page.click('[data-testid="source-select"]');
await page
.getByTestId(`${sourceName}-option`)
.getByText(sourceDisplayName)
.click();
// Filters should reset after source change
await expect(page.getByTestId('filter-select-0')).not.toBeAttached();
// Add owner filter
await page.click('[data-testid="add-filters"]');
await addOwnerFilter({
page,
filterNumber: 0,
ownerName: user.getUserName(),
selectId: 'Owner Name',
});
// Add entityFQN filter
await page.click('[data-testid="add-filters"]');
await addEntityFQNFilter({
page,
filterNumber: 1,
entityFQN: (pipeline.entityResponseData as { fullyQualifiedName: string })
.fullyQualifiedName,
selectId: 'Pipeline Name',
exclude: true,
});
// Add domain filter
await page.click('[data-testid="add-filters"]');
await addDomainFilter({
page,
filterNumber: 2,
domainName: domain.responseData.name,
domainDisplayName: domain.responseData.displayName,
});
// Add trigger
await page.click('[data-testid="add-trigger"]');
await addPipelineStatusUpdatesAction({
page,
filterNumber: 0,
statusName: 'Successful',
exclude: true,
});
// Add multiple destinations
await page.click('[data-testid="add-destination-button"]');
await addInternalDestination({
page,
destinationNumber: 0,
category: 'Owners',
type: 'G Chat',
});
// Add team Slack destination
await page.click('[data-testid="add-destination-button"]');
await addInternalDestination({
page,
destinationNumber: 1,
category: 'Teams',
type: 'Slack',
typeId: 'Team-select',
searchText: 'Organization',
});
};
export const createCommonObservabilityAlert = async ({
page,
alertName,
alertDetails,
sourceName,
sourceDisplayName,
filters,
actions,
}: {
page: Page;
alertName: string;
alertDetails: ObservabilityCreationDetails;
sourceName: string;
sourceDisplayName: string;
filters: {
name: string;
inputSelector: string;
inputValue: string;
inputValueId?: string;
exclude?: boolean;
}[];
actions: {
name: string;
exclude?: boolean;
inputs?: Array<{
inputSelector: string;
inputValue: string;
waitForAPI?: boolean;
}>;
}[];
}) => {
await inputBasicAlertInformation({
page,
name: alertName,
sourceName,
sourceDisplayName,
createButtonId: 'create-observability',
});
for (const filter of filters) {
const filterNumber = filters.indexOf(filter);
await page.click('[data-testid="add-filters"]');
// Select filter
await page.click(`[data-testid="filter-select-${filterNumber}"]`);
await page.click(
`.ant-select-dropdown:visible [data-testid="${filter.name}-filter-option"]`
);
// Search and select filter input value
const searchOptions = page.waitForResponse('/api/v1/search/query?q=*');
await page.fill(
`[data-testid="${filter.inputSelector}"] [role="combobox"]`,
filter.inputValue,
{
force: true,
}
);
await searchOptions;
await page.click(
`.ant-select-dropdown:visible [title="${
filter.inputValueId ?? filter.inputValue
}"]`
);
// Check if option is selected
await expect(
page.locator(
`[data-testid="${filter.inputSelector}"] [title="${
filter.inputValueId ?? filter.inputValue
}"]`
)
).toBeAttached();
if (filter.exclude) {
// Change filter effect
await page.click(`[data-testid="filter-switch-${filterNumber}"]`);
}
}
// Add triggers
for (const action of actions) {
const actionNumber = actions.indexOf(action);
await page.click('[data-testid="add-trigger"]');
// Select action
await page.click(`[data-testid="trigger-select-${actionNumber}"]`);
// Adding the dropdown visibility check to avoid flakiness here
await page.waitForSelector(`.ant-select-dropdown:visible`, {
state: 'visible',
});
await page.click(
`.ant-select-dropdown:visible [data-testid="${action.name}-filter-option"]:visible`
);
await page.waitForSelector(`.ant-select-dropdown:visible`, {
state: 'hidden',
});
if (action.inputs && action.inputs.length > 0) {
for (const input of action.inputs) {
const getSearchResult = page.waitForResponse(
'/api/v1/search/query?q=*'
);
await page.fill(
`[data-testid="${input.inputSelector}"] [role="combobox"]`,
input.inputValue,
{
force: true,
}
);
if (input.waitForAPI) {
await getSearchResult;
}
await page.click(`[title="${input.inputValue}"]:visible`);
// eslint-disable-next-line jest/no-conditional-expect
await expect(page.getByTestId(input.inputSelector)).toHaveText(
input.inputValue
);
await clickOutside(page);
}
}
if (action.exclude) {
// Change filter effect
await page.click(`[data-testid="trigger-switch-${actionNumber}"]`);
}
}
// Add Destinations
for (const destination of alertDetails.destinations) {
const destinationNumber = alertDetails.destinations.indexOf(destination);
await page.click('[data-testid="add-destination-button"]');
if (destination.mode === 'internal') {
await addInternalDestination({
page,
destinationNumber,
category: destination.category,
type: destination.type,
typeId: destination.inputSelector,
searchText: destination.inputValue,
});
} else {
await addExternalDestination({
page,
destinationNumber,
category: destination.category,
input: destination.inputValue,
secretKey: destination.secretKey,
});
}
}
};
export const checkAlertConfigDetails = async ({
page,
sourceName,
tableName,
}: {
page: Page;
sourceName: string;
tableName: string;
}) => {
// Verify alert configs
await expect(page.getByTestId('source-select')).toHaveText(sourceName);
await expect(page.getByTestId('filter-select-0')).toHaveText('Table Name');
await expect(
page.getByTestId('fqn-list-select').getByTitle(tableName)
).toBeAttached();
await expect(
page
.getByTestId('trigger-select-0')
.getByTestId('Get Schema Changes-filter-option')
).toBeAttached();
await expect(
page
.getByTestId('destination-category-select-0')
.getByTestId('Slack-external-option')
).toBeAttached();
await expect(page.getByTestId('endpoint-input-0')).toHaveValue(
'https://slack.com'
);
};
export const checkAlertFlowForWithoutPermissionUser = async ({
page,
alertDetails,
sourceName,
table,
}: {
page: Page;
alertDetails: AlertDetails;
sourceName: string;
table: TableClass;
}) => {
await visitObservabilityAlertPage(page);
await expect(page.getByTestId('create-observability')).not.toBeAttached();
await expect(
page.getByTestId(`alert-edit-${alertDetails.name}`)
).not.toBeAttached();
await expect(
page.getByTestId(`alert-delete-${alertDetails.name}`)
).not.toBeAttached();
// Wait for events to finish execution
await waitForRecentEventsToFinishExecution(page, alertDetails.name, 1);
await visitAlertDetailsPage(page, alertDetails);
await expect(page.getByTestId('edit-owner')).not.toBeAttached();
await expect(page.getByTestId('edit-description')).not.toBeAttached();
await expect(page.getByTestId('edit-button')).not.toBeAttached();
await expect(page.getByTestId('delete-button')).not.toBeAttached();
await checkAlertConfigDetails({
page,
sourceName,
tableName: table.entity.name,
});
await checkRecentEventDetails({
page,
alertDetails,
table,
totalEventsCount: 1,
});
};
export const checkAlertDetailsForWithPermissionUser = async ({
page,
alertDetails,
sourceName,
table,
user,
}: {
page: Page;
alertDetails: AlertDetails;
sourceName: string;
table: TableClass;
user: UserClass;
}) => {
await visitObservabilityAlertPage(page);
await visitAlertDetailsPage(page, alertDetails);
// Change alert owner
await addMultiOwner({
page,
ownerNames: [user.responseData.displayName],
activatorBtnDataTestId: 'edit-owner',
endpoint: EntityTypeEndpoint.NotificationAlert,
type: 'Users',
});
// UpdateDescription
await updateDescription(page, ALERT_UPDATED_DESCRIPTION, true);
// Check other configs
await checkAlertConfigDetails({
page,
sourceName,
tableName: table.entity.name,
});
await checkRecentEventDetails({
page,
alertDetails,
table,
totalEventsCount: 1,
});
};