This is an automated email from the ASF dual-hosted git repository.

rahulvats pushed a commit to branch py-client-sync
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 7df7a83b2f4abd77c7ed6cbb9e76ad40eee1979f
Author: Kevin Yang <[email protected]>
AuthorDate: Tue Mar 24 08:35:22 2026 -0400

    improve e2e tests for dag audit log (#63463)
    
    * improve e2e tests for dag audit log
    
    * refine matching on filter
    
    * improve by never snapshot the DOM to assert against the snapshot
    
    * fix skeleton load
    
    * Fix filter input locator and stabilize dag audit log e2e tests
    
    setFilterValue() previously used getByPlaceholder(filterLabel) to find the
    filter input after selecting a filter from the menu. This never matched
    because InputWithAddon renders the label as a visible <Text> sibling of the
    <input>, not as a placeholder attribute. The filter configs (e.g. 
EVENT_TYPE,
    DAG_ID) do not define a placeholder field, so filter.config.placeholder is
    undefined and React omits the attribute from the DOM entirely.
    
    The fix uses the text-matching approach already established in XComsPage:
    
      page.locator("div").filter({ hasText: `${filterLabel}:` 
}).locator("input").first()
    
    Note: getByPlaceholder() could be restored once the relevant filter configs
    in filterConfigs.tsx expose a placeholder value, or once InputWithAddon 
gains
    an aria-label prop (enabling the preferred getByLabel() approach). Both
    require a coordinated source change discussed with UI maintainers.
    
    Additional stability improvements:
    - Raise addFilter() menu visibility timeout 5s → 10s (CI headroom)
    - Raise setFilterValue() input visibility timeout 5s → 10s (CI headroom)
    - Raise dag-audit-log individual test timeout 60s → 120s; waitForTableLoad()
      can consume up to 60s internally, leaving no room for navigation + 
assertions
      under the old limit on loaded CI runners
    
    * use getByRole instead of hasText
    
    * fix filterInput, locate the container and get text
    
    * improve verify audit log entries by explicit matching patterns
---
 .../src/airflow/ui/tests/e2e/pages/EventsPage.ts   | 116 +++++++--------------
 .../ui/tests/e2e/specs/dag-audit-log.spec.ts       |  52 +++------
 2 files changed, 51 insertions(+), 117 deletions(-)

diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/EventsPage.ts 
b/airflow-core/src/airflow/ui/tests/e2e/pages/EventsPage.ts
index 965d7e9cefc..d9bbf727420 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/EventsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/EventsPage.ts
@@ -32,17 +32,17 @@ export class EventsPage extends BasePage {
 
   public constructor(page: Page) {
     super(page);
-    this.eventsPageTitle = page.locator('h2:has-text("Audit Log")');
-    this.eventsTable = page.locator('[data-testid="table-list"]');
-    this.eventColumn = this.eventsTable.locator('th:has-text("Event")');
-    this.extraColumn = this.eventsTable.locator('th:has-text("Extra")');
+    this.eventsPageTitle = page.getByRole("heading", { level: 2, name: "Audit 
Log" });
+    this.eventsTable = page.getByTestId("table-list");
+    this.eventColumn = this.eventsTable.getByRole("columnheader").filter({ 
hasText: "Event" });
+    this.extraColumn = this.eventsTable.getByRole("columnheader").filter({ 
hasText: "Extra" });
     this.filterBar = page
       .locator("div")
-      .filter({ has: page.locator('button:has-text("Filter")') })
+      .filter({ has: page.getByTestId("add-filter-button") })
       .first();
-    this.ownerColumn = this.eventsTable.locator('th:has-text("User")');
-    this.tableRows = this.eventsTable.locator("tbody tr");
-    this.whenColumn = this.eventsTable.locator('th:has-text("When")');
+    this.ownerColumn = this.eventsTable.getByRole("columnheader").filter({ 
hasText: "User" });
+    this.tableRows = this.eventsTable.locator("tbody").getByRole("row");
+    this.whenColumn = this.eventsTable.getByRole("columnheader").filter({ 
hasText: "When" });
   }
 
   public static getEventsUrl(dagId: string): string {
@@ -50,15 +50,15 @@ export class EventsPage extends BasePage {
   }
 
   public async addFilter(filterName: string): Promise<void> {
-    const filterButton = this.page.locator('button:has-text("Filter")');
+    const filterButton = this.page.getByTestId("add-filter-button");
 
     await filterButton.click();
 
-    const filterMenu = this.page.locator('[role="menu"][data-state="open"]');
+    const filterMenu = this.page.getByRole("menu");
 
-    await filterMenu.waitFor({ state: "visible", timeout: 5000 });
+    await expect(filterMenu).toBeVisible({ timeout: 10_000 });
 
-    const menuItem = 
filterMenu.locator(`[role="menuitem"]:has-text("${filterName}")`);
+    const menuItem = filterMenu.getByRole("menuitem", { name: filterName });
 
     await menuItem.click();
   }
@@ -106,7 +106,7 @@ export class EventsPage extends BasePage {
   }
 
   public getFilterPill(filterLabel: string): Locator {
-    return this.page.locator(`button:has-text("${filterLabel}:")`);
+    return this.page.getByRole("button", { name: `${filterLabel}:` });
   }
 
   public async getTableRowCount(): Promise<number> {
@@ -132,54 +132,40 @@ export class EventsPage extends BasePage {
   public async setFilterValue(filterLabel: string, value: string): 
Promise<void> {
     const filterPill = this.getFilterPill(filterLabel);
 
-    if ((await filterPill.count()) > 0) {
+    if (await filterPill.isVisible()) {
       await filterPill.click();
     }
 
-    // Wait for input to appear and fill it
-    const filterInput = this.page.locator(`input[placeholder*="${filterLabel}" 
i], input`).last();
+    const filterInput = this.page
+      .locator("div")
+      .filter({ has: this.page.getByText(`${filterLabel}:`) })
+      .locator("input")
+      .first();
 
-    await filterInput.waitFor({ state: "visible", timeout: 5000 });
+    await expect(filterInput).toBeVisible({ timeout: 10_000 });
     await filterInput.fill(value);
     await filterInput.press("Enter");
     await this.waitForTableLoad();
   }
 
   public async verifyLogEntriesWithData(): Promise<void> {
-    const rows = await this.getEventLogRows();
-
-    if (rows.length === 0) {
-      throw new Error("No log entries found");
-    }
-
-    const [firstRow] = rows;
-
-    if (!firstRow) {
-      throw new Error("First row is undefined");
-    }
+    await expect(this.tableRows).not.toHaveCount(0);
 
+    const firstRow = this.tableRows.first();
     const whenCell = await this.getCellByColumnName(firstRow, "When");
     const eventCell = await this.getCellByColumnName(firstRow, "Event");
     const userCell = await this.getCellByColumnName(firstRow, "User");
 
-    const whenText = await whenCell.textContent();
-    const eventText = await eventCell.textContent();
-    const userText = await userCell.textContent();
-
-    expect(whenText?.trim()).toBeTruthy();
-    expect(eventText?.trim()).toBeTruthy();
-    expect(userText?.trim()).toBeTruthy();
+    await expect(whenCell).toHaveText(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
+    await expect(eventCell).toHaveText(/[a-z][_a-z]*/);
+    await expect(userCell).toHaveText(/\w+/);
   }
 
   public async verifyTableColumns(): Promise<void> {
-    const headers = await this.eventsTable.locator("thead 
th").allTextContents();
-    const expectedColumns = ["When", "Event", "User", "Extra"];
-
-    for (const col of expectedColumns) {
-      if (!headers.some((h) => h.toLowerCase().includes(col.toLowerCase()))) {
-        throw new Error(`Expected column "${col}" not found in headers: 
${headers.join(", ")}`);
-      }
-    }
+    await expect(this.whenColumn).toBeVisible();
+    await expect(this.eventColumn).toBeVisible();
+    await expect(this.ownerColumn).toBeVisible();
+    await expect(this.extraColumn).toBeVisible();
   }
 
   public async waitForEventsTable(): Promise<void> {
@@ -190,44 +176,20 @@ export class EventsPage extends BasePage {
    * Wait for table to finish loading
    */
   public async waitForTableLoad(): Promise<void> {
-    await this.eventsTable.waitFor({ state: "visible", timeout: 60_000 });
-
-    await this.page.waitForFunction(
-      () => {
-        const table = document.querySelector('[data-testid="table-list"]');
+    await expect(this.eventsTable).toBeVisible({ timeout: 60_000 });
 
-        if (!table) {
-          return false;
-        }
+    const skeleton = this.eventsTable.locator('[data-testid="skeleton"]');
 
-        const skeletons = table.querySelectorAll('[data-scope="skeleton"]');
+    await expect(skeleton).toHaveCount(0, { timeout: 60_000 });
 
-        if (skeletons.length > 0) {
-          return false;
-        }
+    const noDataMessage = this.page.getByText(/no.*events.*found/iu);
 
-        const rows = table.querySelectorAll("tbody tr");
-
-        for (const row of rows) {
-          const cells = row.querySelectorAll("td");
-          let hasContent = false;
-
-          for (const cell of cells) {
-            if (cell.textContent && cell.textContent.trim().length > 0) {
-              hasContent = true;
-              break;
-            }
-          }
-
-          if (!hasContent) {
-            return false;
-          }
-        }
+    await expect(async () => {
+      const rowCount = await this.tableRows.count();
 
-        return true;
-      },
-      undefined,
-      { timeout: 60_000 },
-    );
+      await (rowCount > 0
+        ? expect(this.tableRows.first().locator("td").first()).toHaveText(/.+/)
+        : expect(noDataMessage).toBeVisible());
+    }).toPass({ timeout: 60_000 });
   }
 }
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-audit-log.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-audit-log.spec.ts
index 6b6989ad1a8..214e3a93f33 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-audit-log.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-audit-log.spec.ts
@@ -28,7 +28,7 @@ test.describe("DAG Audit Log", () => {
   const triggerCount = 3;
   const expectedEventCount = triggerCount + 1;
 
-  test.setTimeout(60_000);
+  test.setTimeout(120_000);
 
   test.beforeAll(async ({ browser }) => {
     test.setTimeout(3 * 60 * 1000);
@@ -42,20 +42,11 @@ test.describe("DAG Audit Log", () => {
     }
 
     await setupEventsPage.navigateToAuditLog(testDagId);
-    await page.waitForFunction(
-      (minCount) => {
-        const table = document.querySelector('[data-testid="table-list"]');
+    await expect(async () => {
+      const count = await setupEventsPage.tableRows.count();
 
-        if (!table) {
-          return false;
-        }
-        const rows = table.querySelectorAll("tbody tr");
-
-        return rows.length >= minCount;
-      },
-      expectedEventCount,
-      { timeout: 60_000 },
-    );
+      expect(count).toBeGreaterThanOrEqual(expectedEventCount);
+    }).toPass({ timeout: 60_000 });
 
     await context.close();
   });
@@ -68,10 +59,7 @@ test.describe("DAG Audit Log", () => {
     await eventsPage.navigateToAuditLog(testDagId);
 
     await expect(eventsPage.eventsTable).toBeVisible();
-
-    const rowCount = await eventsPage.tableRows.count();
-
-    expect(rowCount).toBeGreaterThan(0);
+    await expect(eventsPage.tableRows).not.toHaveCount(0);
   });
 
   test("verify expected columns are visible", async () => {
@@ -82,7 +70,7 @@ test.describe("DAG Audit Log", () => {
     await expect(eventsPage.ownerColumn).toBeVisible();
     await expect(eventsPage.extraColumn).toBeVisible();
 
-    const dagIdColumn = eventsPage.eventsTable.locator('th:has-text("DAG 
ID")');
+    const dagIdColumn = 
eventsPage.eventsTable.getByRole("columnheader").filter({ hasText: "DAG ID" });
 
     await expect(dagIdColumn).not.toBeVisible();
   });
@@ -90,31 +78,15 @@ test.describe("DAG Audit Log", () => {
   test("verify audit log entries display valid data", async () => {
     await eventsPage.navigateToAuditLog(testDagId);
 
-    const rows = await eventsPage.getEventLogRows();
-
-    expect(rows.length).toBeGreaterThan(0);
-
-    const [firstRow] = rows;
-
-    if (!firstRow) {
-      throw new Error("No rows found");
-    }
+    await expect(eventsPage.tableRows).not.toHaveCount(0);
 
+    const firstRow = eventsPage.tableRows.first();
     const whenCell = await eventsPage.getCellByColumnName(firstRow, "When");
     const eventCell = await eventsPage.getCellByColumnName(firstRow, "Event");
     const userCell = await eventsPage.getCellByColumnName(firstRow, "User");
 
-    const whenText = await whenCell.textContent();
-    const eventText = await eventCell.textContent();
-    const userText = await userCell.textContent();
-
-    expect(whenText).toBeTruthy();
-    expect(whenText?.trim()).not.toBe("");
-
-    expect(eventText).toBeTruthy();
-    expect(eventText?.trim()).not.toBe("");
-
-    expect(userText).toBeTruthy();
-    expect(userText?.trim()).not.toBe("");
+    await expect(whenCell).toHaveText(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
+    await expect(eventCell).toHaveText(/[a-z][_a-z]*/);
+    await expect(userCell).toHaveText(/\w+/);
   });
 });

Reply via email to