[kbn/response-ops-alerts-table] set data-test-subj for EuiDataGrid based on loading status (#217230)

## Summary

Follow-up to #217153

### Problem Description
In UI tests, there was no reliable way to determine when the Alerts
table content had fully loaded before interacting with it. This could
lead to flaky tests where interactions occurred before the data was
available (rows are not present yet), causing failures or inconsistent
results (checking for row with specific content to exist)


![image](https://github.com/user-attachments/assets/6580f134-0bf2-48b8-8cc9-b6d476f4e932)

Quite often we see tests waiting for global indicator (spinner in the
top left corner) to be hidden as a condition for page loading is
complete. This is quite unreliable approach and testing tools have no
consistent built-in solution: FTR, Cypress or even Playwright - network
idle wait is officially marked as
[discouraged](https://playwright.dev/docs/api/class-page)).
We need to help testing tool to interact with UI components in ready
state only.


### Solution
To address this issue, I modified a `data-test-subj` property in the
`<EuiDataGrid>` component. The property dynamically switches between
`alertsTableIsLoading` when data is still loading and
`alertsTableIsLoaded `once the content is available. This allows UI
tests to wait for precisely `alertsTableIsLoaded` to be in in the DOM
before interacting with the table, ensuring more reliable and stable
test execution.


Passed 10/10
<img width="538" alt="image"
src="https://github.com/user-attachments/assets/e44bae5f-4094-4ed2-89f3-74a52cb2be53"
/>
This commit is contained in:
Dzmitry Lemechko 2025-04-15 13:05:49 +02:00 committed by GitHub
parent 401b76e5eb
commit edf8d6d975
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 23 additions and 20 deletions

View file

@ -358,7 +358,7 @@ export const AlertsDataGrid = typedMemo(
ref={dataGridRef}
css={rowStyles}
aria-label="Alerts table"
data-test-subj="alertsTable"
data-test-subj={isLoading ? `alertsTableIsLoading` : `alertsTableIsLoaded`}
height={height}
columns={columnsWithCellActions}
columnVisibility={columnVisibility}

View file

@ -10,16 +10,14 @@ import { ScoutPage, Locator, expect } from '@kbn/scout';
const PAGE_URL = 'security/alerts';
export class AlertsTablePage {
public detectionsAlertsContainer: Locator;
public detectionsAlertsWrapper: Locator;
public alertRow: Locator;
public alertsTableBody: Locator;
public alertsTable: Locator;
constructor(private readonly page: ScoutPage) {
this.detectionsAlertsContainer = this.page.testSubj.locator('detectionsAlertsPage');
this.detectionsAlertsWrapper = this.page.testSubj.locator('detectionsAlertsPage');
this.alertRow = this.page.locator('div.euiDataGridRow');
this.alertsTableBody = this.page.testSubj
.locator('alertsTable')
.locator(`[data-test-subj='euiDataGridBody']`);
this.alertsTable = this.page.testSubj.locator('alertsTableIsLoaded'); // Search for loaded Alerts table
}
async navigate() {
@ -27,7 +25,7 @@ export class AlertsTablePage {
}
async expandAlertDetailsFlyout(ruleName: string) {
await this.alertsTableBody.waitFor({ state: 'visible' });
await this.alertsTable.waitFor({ state: 'visible' });
// Filter alert by unique rule name
const row = this.alertRow.filter({ hasText: ruleName });
await expect(
@ -37,4 +35,9 @@ export class AlertsTablePage {
return row.locator(`[data-test-subj='expand-event']`).click();
}
async waitForDetectionsAlertsWrapper() {
// Increased timeout to 20 seconds because this page sometimes takes longer to load
return this.detectionsAlertsWrapper.waitFor({ state: 'visible', timeout: 20_000 });
}
}

View file

@ -139,7 +139,7 @@ describe('Isolate command', { tags: ['@ess', '@serverless', '@brokenInServerless
loadPage(APP_ALERTS_PATH);
closeAllToasts();
cy.getByTestSubj('alertsTable').within(() => {
cy.getByTestSubj('alertsTableIsLoaded').within(() => {
cy.getByTestSubj('expand-event')
.first()
.within(() => {

View file

@ -30,7 +30,7 @@ export const getAlertsTableRows = (timeout?: number): Cypress.Chainable<JQuery<H
clickAlertListRefreshButton();
return cy
.getByTestSubj('alertsTable')
.getByTestSubj('alertsTableIsLoaded')
.find<HTMLDivElement>('.euiDataGridRow')
.then(($rowsFound) => {
$rows = $rowsFound;

View file

@ -28,7 +28,7 @@ spaceTest.describe('Expandable flyout state sync', { tag: ['@ess', '@svlSecurity
const urlBeforeAlertDetails = page.url();
expect(urlBeforeAlertDetails).not.toContain(RIGHT);
await pageObjects.alertsTablePage.detectionsAlertsContainer.waitFor({ state: 'visible' });
await pageObjects.alertsTablePage.waitForDetectionsAlertsWrapper();
await pageObjects.alertsTablePage.expandAlertDetailsFlyout(ruleName);
const urlAfterAlertDetails = page.url();
@ -38,7 +38,7 @@ spaceTest.describe('Expandable flyout state sync', { tag: ['@ess', '@svlSecurity
await expect(headerTitle).toHaveText(ruleName);
await page.reload();
await pageObjects.alertsTablePage.detectionsAlertsContainer.waitFor({ state: 'visible' });
await pageObjects.alertsTablePage.waitForDetectionsAlertsWrapper();
const urlAfterReload = page.url();
expect(urlAfterReload).toContain(RIGHT);

View file

@ -64,7 +64,7 @@ export const testSubjectIds = {
GRAPH_ACTIONS_TOGGLE_SEARCH_ID: 'cloudSecurityGraphGraphInvestigationToggleSearch',
GRAPH_ACTIONS_INVESTIGATE_IN_TIMELINE_ID:
'cloudSecurityGraphGraphInvestigationInvestigateInTimeline',
ALERT_TABLE_ROW_CSS_SELECTOR: '[data-test-subj="alertsTable"] .euiDataGridRow',
ALERT_TABLE_ROW_CSS_SELECTOR: '[data-test-subj="alertsTableIsLoaded"] .euiDataGridRow',
SETUP_TECHNOLOGY_SELECTOR: 'setup-technology-selector',
DIRECT_ACCESS_KEYS: 'direct_access_keys',
SETUP_TECHNOLOGY_SELECTOR_AGENTLESS_RADIO: 'setup-technology-agentless-radio',

View file

@ -21,7 +21,7 @@ const DATE_WITH_DATA = {
const ALERTS_FLYOUT_SELECTOR = 'alertsFlyout';
const FILTER_FOR_VALUE_BUTTON_SELECTOR = 'filterForValue';
const ALERTS_TABLE_WITH_DATA_SELECTOR = 'alertsTable';
const ALERTS_TABLE_WITH_DATA_SELECTOR = 'alertsTableIsLoaded';
const ALERTS_TABLE_NO_DATA_SELECTOR = 'alertsTableEmptyState';
const ALERTS_TABLE_ERROR_PROMPT_SELECTOR = 'alertsTableErrorPrompt';
const ALERTS_TABLE_ACTIONS_MENU_SELECTOR = 'alertsTableActionsMenu';

View file

@ -22,7 +22,7 @@ const ALERTS_TITLE = 'Alerts';
const ALERTS_ACCORDION_SELECTOR = `accordion-${ALERTS_TITLE}`;
const ALERTS_SECTION_BUTTON_CSS_SELECTOR = `[data-test-subj=${ALERTS_ACCORDION_SELECTOR}] button.euiAccordion__button`;
const ALERTS_TABLE_NO_DATA_SELECTOR = 'alertsTableEmptyState';
const ALERTS_TABLE_WITH_DATA_SELECTOR = 'alertsTable';
const ALERTS_TABLE_WITH_DATA_SELECTOR = 'alertsTableIsLoaded';
const ALERTS_TABLE_LOADING_SELECTOR = 'internalAlertsPageLoading';
export function ObservabilityOverviewCommonProvider({

View file

@ -41,7 +41,7 @@ describe('Alert Table API calls', { tags: ['@ess', '@serverless'] }, () => {
});
it('should call `api/lists/index` only once', () => {
cy.get('[data-test-subj="alertsTable"]').then(() => {
cy.get('[data-test-subj="alertsTableIsLoaded"]').then(() => {
expect(callCount, 'number of times lists index api is called').to.equal(1);
});
});

View file

@ -38,7 +38,7 @@ export const GET_DATA_GRID_HEADER_CELL_ACTION_GROUP = (fieldName: string) => {
};
export const DATA_GRID_FULL_SCREEN =
'[data-test-subj="alertsTable"] [data-test-subj="dataGridFullScreenButton"]';
'[data-test-subj="alertsTableIsLoaded"] [data-test-subj="dataGridFullScreenButton"]';
export const DATA_GRID_FIELD_SORT_BTN = '[data-test-subj="dataGridColumnSortingButton"]';

View file

@ -87,7 +87,7 @@ export const NEW_TERMS_FIELDS_DETAILS = 'Fields';
export const NEW_TERMS_HISTORY_WINDOW_DETAILS = 'History Window Size';
export const FIELDS_BROWSER_BTN =
'[data-test-subj="alertsTable"] [data-test-subj="show-field-browser"]';
'[data-test-subj="alertsTableIsLoaded"] [data-test-subj="show-field-browser"]';
export const LAST_EXECUTION_STATUS_REFRESH_BUTTON =
'[data-test-subj="ruleLastExecutionStatusRefreshButton"]';

View file

@ -489,7 +489,7 @@ export const sumAlertCountFromAlertCountTable = (callback?: (sumOfEachRow: numbe
};
export const selectFirstPageAlerts = () => {
const ALERTS_DATA_GRID = '[data-test-subj="alertsTable"]';
const ALERTS_DATA_GRID = '[data-test-subj="alertsTableIsLoaded"]';
cy.get(ALERTS_DATA_GRID).find(SELECT_ALL_VISIBLE_ALERTS).scrollIntoView();
cy.get(ALERTS_DATA_GRID).find(SELECT_ALL_VISIBLE_ALERTS).click({ force: true });
};

View file

@ -8,7 +8,7 @@
import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services';
import { FtrService } from '../../../functional/ftr_provider_context';
const ALERT_TABLE_ROW_CSS_SELECTOR = '[data-test-subj="alertsTable"] .euiDataGridRow';
const ALERT_TABLE_ROW_CSS_SELECTOR = '[data-test-subj="alertsTableIsLoaded"] .euiDataGridRow';
export class DetectionsPageObject extends FtrService {
private readonly find = this.ctx.getService('find');