Repository: elastic/kibana
Pull request #216262: [scout/security] improve alerting table wait
Author: login: dmlemeshko
Labels: name: release_note:skip
description: Skip the PR/issue when compiling release notes
name: v9.0.0
name: backport:version
description: Backport to applied version labels
name: test:scout
name: v9.1.0
name: v8.19.0
Created: 2025-03-28T09:27:08Z | Merged: 2025-04-03T14:37:40Z

## Summary

This PR improves waiting for Alerts table to be loaded.

Relying on the first alert might cause flakiness in the future test cases, I changed the rule name to be unique for test run and search alert based on its rule name, not the first row.

If the alert is missing or it has a wrong name, the test will fail with clear message:

```
    Error: Alert with rule 'xys' is not displayed

    Timed out 10000ms waiting for expect(locator).toBeVisible()

    Locator: locator('div.euiDataGridRow').filter({ hasText: 'xys' })
    Expected: visible
    Received: <element(s) not found>
    Call log:
      - Alert with rule 'xys' is not displayed with timeout 10000ms
      - waiting for locator('div.euiDataGridRow').filter({ hasText: 'xys' })
```

<!--ONMERGE {"backportTargets":["8.x","9.0"]} ONMERGE-->


Diff:
diff --git a/x-pack/solutions/security/packages/kbn-scout-security/src/playwright/fixtures/test/page_objects/alerts_table.ts b/x-pack/solutions/security/packages/kbn-scout-security/src/playwright/fixtures/test/page_objects/alerts_table.ts
index e07c454450280..cb3cf8323cd6b 100644
--- a/x-pack/solutions/security/packages/kbn-scout-security/src/playwright/fixtures/test/page_objects/alerts_table.ts
+++ b/x-pack/solutions/security/packages/kbn-scout-security/src/playwright/fixtures/test/page_objects/alerts_table.ts
@@ -5,48 +5,33 @@
  * 2.0.
  */
 
-import { ScoutPage, Locator } from '@kbn/scout';
+import { ScoutPage, Locator, expect } from '@kbn/scout';
 
 const PAGE_URL = 'security/alerts';
-const EXPAND_EVENT = 'expand-event';
 
 export class AlertsTablePage {
-  public expandAlertBtn: Locator;
+  public alertRow: Locator;
+  public alertsTable: Locator;
 
   constructor(private readonly page: ScoutPage) {
-    this.expandAlertBtn = this.page.testSubj.locator(EXPAND_EVENT);
+    this.alertRow = this.page.locator('div.euiDataGridRow');
+    this.alertsTable = this.page.testSubj.locator('alertsTable');
   }
 
   async navigate() {
-    await this.page.gotoApp(PAGE_URL);
+    return this.page.gotoApp(PAGE_URL);
   }
 
-  async expandFirstAlertDetailsFlyout() {
-    const maxAttempts = 100;
-    let attempts = 0;
-
-    while ((await this.expandAlertBtn.count()) === 0) {
-      if (attempts >= maxAttempts) {
-        throw new Error('Timed out waiting for alert buttons to appear');
-      }
-      await this.page.waitForTimeout(100);
-      attempts++;
-    }
-    const buttons = await this.expandAlertBtn.all();
-
-    if (buttons.length > 0) {
-      await buttons[0].click();
-    } else {
-      throw new Error('No expand alert details buttons found');
-    }
-  }
-
-  async getCurrentUrl() {
-    const url = this.page.url();
-    return url;
-  }
-
-  async reload() {
-    return this.page.reload();
+  async expandAlertDetailsFlyout(ruleName: string) {
+    await this.page.waitForLoadingIndicatorHidden();
+    await this.alertsTable.waitFor({ state: 'visible' });
+    // Filter alert by unique rule name
+    const row = this.alertRow.filter({ hasText: ruleName });
+    await expect(
+      row,
+      `Alert with rule '${ruleName}' is not displayed in the alerts table`
+    ).toBeVisible();
+
+    return row.locator(`[data-test-subj='expand-event']`).click();
   }
 }
diff --git a/x-pack/solutions/security/plugins/security_solution/ui_tests/parallel_tests/flyout/alert_details_url_sync.spec.ts b/x-pack/solutions/security/plugins/security_solution/ui_tests/parallel_tests/flyout/alert_details_url_sync.spec.ts
index e67e7986374eb..eefaaab2c246b 100644
--- a/x-pack/solutions/security/plugins/security_solution/ui_tests/parallel_tests/flyout/alert_details_url_sync.spec.ts
+++ b/x-pack/solutions/security/plugins/security_solution/ui_tests/parallel_tests/flyout/alert_details_url_sync.spec.ts
@@ -11,34 +11,40 @@ import { CUSTOM_QUERY_RULE } from '@kbn/scout-security/src/playwright/constants/
 const RIGHT = 'right';
 
 spaceTest.describe('Expandable flyout state sync', { tag: ['@ess', '@svlSecurity '] }, () => {
-  spaceTest.beforeEach(async ({ browserAuth, apiServices }) => {
-    await apiServices.detectionRule.deleteAll();
-    await apiServices.detectionRule.createCustomQueryRule(CUSTOM_QUERY_RULE);
+  let ruleName: string;
+  spaceTest.beforeEach(async ({ browserAuth, apiServices, scoutSpace }) => {
+    ruleName = `${CUSTOM_QUERY_RULE.name}_${scoutSpace.id}_${Date.now()}`;
+    await apiServices.detectionRule.createCustomQueryRule({ ...CUSTOM_QUERY_RULE, name: ruleName });
     await browserAuth.loginAsPlatformEngineer();
   });
 
-  spaceTest('should test flyout url sync', async ({ pageObjects }) => {
+  spaceTest.afterEach(async ({ apiServices }) => {
+    await apiServices.detectionRule.deleteAll();
+  });
+
+  spaceTest('should test flyout url sync', async ({ pageObjects, page }) => {
     await pageObjects.alertsTablePage.navigate();
 
-    const urlBeforeAlertDetails = await pageObjects.alertsTablePage.getCurrentUrl();
+    const urlBeforeAlertDetails = page.url();
     expect(urlBeforeAlertDetails).not.toContain(RIGHT);
 
-    await pageObjects.alertsTablePage.expandFirstAlertDetailsFlyout();
+    await pageObjects.alertsTablePage.expandAlertDetailsFlyout(ruleName);
 
-    const urlAfterAlertDetails = await pageObjects.alertsTablePage.getCurrentUrl();
+    const urlAfterAlertDetails = page.url();
     expect(urlAfterAlertDetails).toContain(RIGHT);
 
     const headerTitle = pageObjects.alertDetailsRightPanelPage.detailsFlyoutHeaderTitle;
-    await expect(headerTitle).toHaveText(CUSTOM_QUERY_RULE.name);
+    await expect(headerTitle).toHaveText(ruleName);
 
-    await pageObjects.alertsTablePage.reload();
+    await page.reload();
+    await page.waitForLoadingIndicatorHidden();
 
-    const urlAfterReload = await pageObjects.alertsTablePage.getCurrentUrl();
+    const urlAfterReload = page.url();
     expect(urlAfterReload).toContain(RIGHT);
 
     await pageObjects.alertDetailsRightPanelPage.closeFlyout();
 
-    const urlAfterClosingFlyout = await pageObjects.alertsTablePage.getCurrentUrl();
+    const urlAfterClosingFlyout = page.url();
     expect(urlAfterClosingFlyout).not.toContain(RIGHT);
   });
 });


Reviews:
user: login: csr
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T09:56:59Z
user: login: csr
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T10:03:19Z
user: login: csr
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-02T10:04:24Z
user: login: dmlemeshko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T11:50:09Z
user: login: MadameSheema
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T14:36:42Z

Review comments:
diff_hunk: @@ -11,34 +11,40 @@ import { CUSTOM_QUERY_RULE } from '@kbn/scout-security/src/playwright/constants/
 const RIGHT = 'right';
 
 spaceTest.describe('Expandable flyout state sync', { tag: ['@ess', '@svlSecurity '] }, () => {
-  spaceTest.beforeEach(async ({ browserAuth, apiServices }) => {
-    await apiServices.detectionRule.deleteAll();
-    await apiServices.detectionRule.createCustomQueryRule(CUSTOM_QUERY_RULE);
+  let ruleName: string;
+  spaceTest.beforeEach(async ({ browserAuth, apiServices, scoutSpace }) => {
+    ruleName = `${CUSTOM_QUERY_RULE.name}_${scoutSpace.id}_${Date.now()}`;
+    await apiServices.detectionRule.createCustomQueryRule({ ...CUSTOM_QUERY_RULE, name: ruleName });
     await browserAuth.loginAsPlatformEngineer();
   });
 
-  spaceTest('should test flyout url sync', async ({ pageObjects }) => {
+  spaceTest.afterEach(async ({ apiServices }) => {
+    await apiServices.detectionRule.deleteAll();
+  });
+
+  spaceTest('should test flyout url sync', async ({ pageObjects, page }) => {
     await pageObjects.alertsTablePage.navigate();
 
-    const urlBeforeAlertDetails = await pageObjects.alertsTablePage.getCurrentUrl();
+    const urlBeforeAlertDetails = page.url();
path: x-pack/solutions/security/plugins/security_solution/ui_tests/parallel_tests/flyout/alert_details_url_sync.spec.ts
user: login: dmlemeshko
body: I believe it is simpler to call directly in test + it is a sync call
created_at: 2025-03-31T16:45:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,34 +11,40 @@ import { CUSTOM_QUERY_RULE } from '@kbn/scout-security/src/playwright/constants/
 const RIGHT = 'right';
 
 spaceTest.describe('Expandable flyout state sync', { tag: ['@ess', '@svlSecurity '] }, () => {
-  spaceTest.beforeEach(async ({ browserAuth, apiServices }) => {
-    await apiServices.detectionRule.deleteAll();
-    await apiServices.detectionRule.createCustomQueryRule(CUSTOM_QUERY_RULE);
+  let ruleName: string;
+  spaceTest.beforeEach(async ({ browserAuth, apiServices, scoutSpace }) => {
+    ruleName = `${CUSTOM_QUERY_RULE.name}_${scoutSpace.id}_${Date.now()}`;
+    await apiServices.detectionRule.createCustomQueryRule({ ...CUSTOM_QUERY_RULE, name: ruleName });
     await browserAuth.loginAsPlatformEngineer();
   });
 
-  spaceTest('should test flyout url sync', async ({ pageObjects }) => {
+  spaceTest.afterEach(async ({ apiServices }) => {
+    await apiServices.detectionRule.deleteAll();
+  });
+
+  spaceTest('should test flyout url sync', async ({ pageObjects, page }) => {
     await pageObjects.alertsTablePage.navigate();
path: x-pack/solutions/security/plugins/security_solution/ui_tests/parallel_tests/flyout/alert_details_url_sync.spec.ts
user: login: csr
body: Question: should we `await page.waitForLoadingIndicatorHidden()` after we `navigate()` to a page?

I ask this because I saw we do wait for the loading indicator to go away after we `reload()` later in the test.

---

**Update**: on a second look, I see we already wait for the page indicator to go away inside `expandAlertDetailsFlyout`, so perhaps that should be enough then.
created_at: 2025-04-02T09:56:58Z
author_association: MEMBER
diff_hunk: @@ -5,48 +5,30 @@
  * 2.0.
  */
 
-import { ScoutPage, Locator } from '@kbn/scout';
+import { ScoutPage, Locator, expect } from '@kbn/scout';
 
 const PAGE_URL = 'security/alerts';
-const EXPAND_EVENT = 'expand-event';
 
 export class AlertsTablePage {
-  public expandAlertBtn: Locator;
+  public alertRow: Locator;
+  public alertsTable: Locator;
 
   constructor(private readonly page: ScoutPage) {
-    this.expandAlertBtn = this.page.testSubj.locator(EXPAND_EVENT);
+    this.alertRow = this.page.locator('div.euiDataGridRow');
+    this.alertsTable = this.page.testSubj.locator('alertsTable');
   }
 
   async navigate() {
-    await this.page.gotoApp(PAGE_URL);
+    return this.page.gotoApp(PAGE_URL);
   }
 
-  async expandFirstAlertDetailsFlyout() {
-    const maxAttempts = 100;
-    let attempts = 0;
+  async expandAlertDetailsFlyout(ruleName: string) {
+    await this.page.waitForLoadingIndicatorHidden();
+    await this.alertsTable.waitFor({ state: 'visible' });
+    // Filter alert by unique rule name
+    const row = this.alertRow.filter({ hasText: ruleName });
+    await expect(row, `Alert with rule '${ruleName}' is not displayed`).toBeVisible();
path: x-pack/solutions/security/packages/kbn-scout-security/src/playwright/fixtures/test/page_objects/alerts_table.ts
user: login: csr
body: Very clear error message! Optional: I would possibly add `in the alerts table` at the end to make it super clear that is where we look for the rule name. The error would then be:

> Alert with rule '${ruleName}' is not displayed in the alerts table
created_at: 2025-04-02T10:03:17Z
author_association: MEMBER
diff_hunk: @@ -11,34 +11,40 @@ import { CUSTOM_QUERY_RULE } from '@kbn/scout-security/src/playwright/constants/
 const RIGHT = 'right';
 
 spaceTest.describe('Expandable flyout state sync', { tag: ['@ess', '@svlSecurity '] }, () => {
-  spaceTest.beforeEach(async ({ browserAuth, apiServices }) => {
-    await apiServices.detectionRule.deleteAll();
-    await apiServices.detectionRule.createCustomQueryRule(CUSTOM_QUERY_RULE);
+  let ruleName: string;
+  spaceTest.beforeEach(async ({ browserAuth, apiServices, scoutSpace }) => {
+    ruleName = `${CUSTOM_QUERY_RULE.name}_${scoutSpace.id}_${Date.now()}`;
+    await apiServices.detectionRule.createCustomQueryRule({ ...CUSTOM_QUERY_RULE, name: ruleName });
     await browserAuth.loginAsPlatformEngineer();
   });
 
-  spaceTest('should test flyout url sync', async ({ pageObjects }) => {
+  spaceTest.afterEach(async ({ apiServices }) => {
+    await apiServices.detectionRule.deleteAll();
+  });
+
+  spaceTest('should test flyout url sync', async ({ pageObjects, page }) => {
     await pageObjects.alertsTablePage.navigate();
path: x-pack/solutions/security/plugins/security_solution/ui_tests/parallel_tests/flyout/alert_details_url_sync.spec.ts
user: login: dmlemeshko
body: Good question. I believe we shouldn't use waiters by default, but only when the next action stability requires it.
The reason is time execution will grow without much benefits.
created_at: 2025-04-02T11:49:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: elasticmachine
created_at: 2025-04-02T13:59:11Z
body: ## :yellow_heart: Build succeeded, but was flaky
* [Buildkite Build](https://buildkite.com/elastic/kibana-pull-request/builds/289461)
* Commit: 929f510c1a0ed1210944dcb2cf70c754072321da

### Failed CI Steps
* [Jest Tests #2](https://buildkite.com/elastic/kibana-pull-request/builds/289461#0195f68a-5d49-4a2c-a0e0-6803c7e8225b)

### Test Failures
* [[job]](https://buildkite.com/elastic/kibana-pull-request/builds/289461#0195f68a-5d49-4a2c-a0e0-6803c7e8225b) [[logs]](https://buildkite.com/organizations/elastic/pipelines/kibana-pull-request/builds/289461/jobs/0195f68a-5d49-4a2c-a0e0-6803c7e8225b/artifacts/0195f6af-ff5d-4f03-b3a7-c40c035e2f5f) Jest Tests #<span></span>2 / <TableTab /> should filter the table correctly

### Metrics [[docs](https://www.elastic.co/guide/en/kibana/master/ci-metrics.html)]
> ✅ unchanged

### History
* :green_heart: [Build #288708](https://buildkite.com/elastic/kibana-pull-request/builds/288708) succeeded b6a8dea71921a7c8dfaee00a0284491b5fb65d16
* :broken_heart: [Build #288123](https://buildkite.com/elastic/kibana-pull-request/builds/288123) failed 2f34115b95dc902ba49e6271cc5454df1c901f11
* :broken_heart: [Build #287989](https://buildkite.com/elastic/kibana-pull-request/builds/287989) failed 8d821287d9a83c3c4878b58fc3d47e0f79b2d2a2

cc @dmlemeshko

<!--buildkite-pr-comment-kibana-pull-request
{"builds":[{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/289461","number":289461,"commit":"929f510c1a0ed1210944dcb2cf70c754072321da"},{"buildStatus":{"state":"passed","success":true,"hasRetries":false,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/288708","number":288708,"commit":"b6a8dea71921a7c8dfaee00a0284491b5fb65d16"},{"buildStatus":{"state":"failed","success":false,"hasRetries":true,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/288123","number":288123,"commit":"2f34115b95dc902ba49e6271cc5454df1c901f11"},{"buildStatus":{"state":"failed","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/287989","number":287989,"commit":"8d821287d9a83c3c4878b58fc3d47e0f79b2d2a2"}],"number":289461}
buildkite-pr-comment-->
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-04-03T14:38:02Z
body: Starting backport for target branches: 8.x, 9.0

https://github.com/elastic/kibana/actions/runs/14245753434
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-04-03T14:44:58Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|8.x|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/217038">](https://github.com/elastic/kibana/pull/217038)|
|✅|9.0|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/217039">](https://github.com/elastic/kibana/pull/217039)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR