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

rahulvats pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 2141659f45f Add integration tests for /events Audit log page (#60122)
2141659f45f is described below

commit 2141659f45fdc0f588f11c4e758c11104df7a0e5
Author: Prajwal7842 <[email protected]>
AuthorDate: Wed Feb 25 13:04:46 2026 +0530

    Add integration tests for /events Audit log page (#60122)
    
    Add integration tests for /events Audit log page
---
 .../src/airflow/ui/tests/e2e/pages/EventsPage.ts   | 106 ++++++++++++++-
 .../airflow/ui/tests/e2e/specs/events-page.spec.ts | 147 +++++++++++++++++++++
 2 files changed, 250 insertions(+), 3 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 8fec920236f..35a84242f6f 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/EventsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/EventsPage.ts
@@ -17,12 +17,15 @@
  * under the License.
  */
 import type { Locator, Page } from "@playwright/test";
+import { expect } from "@playwright/test";
 import { BasePage } from "tests/e2e/pages/BasePage";
 
 export class EventsPage extends BasePage {
   public readonly eventColumn: Locator;
+  public readonly eventsPageTitle: Locator;
   public readonly eventsTable: Locator;
   public readonly extraColumn: Locator;
+  public readonly filterBar: Locator;
   public readonly ownerColumn: Locator;
   public readonly paginationNextButton: Locator;
   public readonly paginationPrevButton: Locator;
@@ -34,9 +37,14 @@ 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.filterBar = page
+      .locator("div")
+      .filter({ has: page.locator('button:has-text("Filter")') })
+      .first();
     this.ownerColumn = this.eventsTable.locator('th:has-text("User")');
     this.paginationNextButton = page.locator('[data-testid="next"]');
     this.paginationPrevButton = page.locator('[data-testid="prev"]');
@@ -48,6 +56,28 @@ export class EventsPage extends BasePage {
     return `/dags/${dagId}/events`;
   }
 
+  public async addFilter(filterName: string): Promise<void> {
+    const filterButton = this.page.locator('button:has-text("Filter")');
+
+    await filterButton.click();
+
+    const filterMenu = this.page.locator('[role="menu"][data-state="open"]');
+
+    await filterMenu.waitFor({ state: "visible", timeout: 5000 });
+
+    const menuItem = 
filterMenu.locator(`[role="menuitem"]:has-text("${filterName}")`);
+
+    await menuItem.click();
+  }
+
+  public async clickColumnHeader(columnKey: string): Promise<void> {
+    const columnHeader = this.eventsTable.locator("th").filter({ hasText: new 
RegExp(columnKey, "i") });
+    const sortButton = columnHeader.locator('button[aria-label="sort"]');
+
+    await sortButton.click();
+    await this.waitForTableLoad();
+  }
+
   public async clickColumnToSort(columnName: "Event" | "User" | "When"): 
Promise<void> {
     const columnHeader = 
this.eventsTable.locator(`th:has-text("${columnName}")`);
     const sortButton = columnHeader.locator('button[aria-label="sort"]');
@@ -113,6 +143,7 @@ export class EventsPage extends BasePage {
     }
 
     const allEventTypes = [...eventTypes];
+    const startUrl = this.page.url();
 
     while (await this.hasNextPage()) {
       await this.clickNextPage();
@@ -121,13 +152,20 @@ export class EventsPage extends BasePage {
       allEventTypes.push(...pageEvents);
     }
 
-    while ((await this.paginationPrevButton.count()) > 0 && (await 
this.paginationPrevButton.isEnabled())) {
-      await this.clickPrevPage();
-    }
+    await this.page.goto(startUrl, { timeout: 30_000, waitUntil: 
"domcontentloaded" });
+    await this.waitForTableLoad();
 
     return allEventTypes;
   }
 
+  public getFilterPill(filterLabel: string): Locator {
+    return this.page.locator(`button:has-text("${filterLabel}:")`);
+  }
+
+  public async getTableRowCount(): Promise<number> {
+    return this.tableRows.count();
+  }
+
   public async hasNextPage(): Promise<boolean> {
     const count = await this.paginationNextButton.count();
 
@@ -138,6 +176,11 @@ export class EventsPage extends BasePage {
     return await this.paginationNextButton.isEnabled();
   }
 
+  public async navigate(): Promise<void> {
+    await this.navigateTo("/events");
+    await this.waitForTableLoad();
+  }
+
   public async navigateToAuditLog(dagId: string, limit?: number): 
Promise<void> {
     this.currentDagId = dagId;
     this.currentLimit = limit;
@@ -152,6 +195,63 @@ export class EventsPage extends BasePage {
     await this.waitForTableLoad();
   }
 
+  public async setFilterValue(filterLabel: string, value: string): 
Promise<void> {
+    const filterPill = this.getFilterPill(filterLabel);
+
+    if ((await filterPill.count()) > 0) {
+      await filterPill.click();
+    }
+
+    // Wait for input to appear and fill it
+    const filterInput = this.page.locator(`input[placeholder*="${filterLabel}" 
i], input`).last();
+
+    await filterInput.waitFor({ state: "visible", timeout: 5000 });
+    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");
+    }
+
+    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();
+  }
+
+  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(", ")}`);
+      }
+    }
+  }
+
+  public async waitForEventsTable(): Promise<void> {
+    await this.waitForTableLoad();
+  }
+
   /**
    * Wait for table to finish loading
    */
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/events-page.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/events-page.spec.ts
new file mode 100644
index 00000000000..bd2ae48b677
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/events-page.spec.ts
@@ -0,0 +1,147 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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, test } from "@playwright/test";
+import { testConfig, AUTH_FILE } from "playwright.config";
+import { DagsPage } from "tests/e2e/pages/DagsPage";
+import { EventsPage } from "tests/e2e/pages/EventsPage";
+
+test.describe("Events Page", () => {
+  let eventsPage: EventsPage;
+
+  test.beforeEach(({ page }) => {
+    eventsPage = new EventsPage(page);
+  });
+
+  test("verify events page displays correctly", async () => {
+    await eventsPage.navigate();
+
+    await expect(eventsPage.eventsPageTitle).toBeVisible({ timeout: 10_000 });
+    await expect(eventsPage.eventsTable).toBeVisible();
+    await eventsPage.verifyTableColumns();
+  });
+
+  test("verify search input is visible", async () => {
+    await eventsPage.navigate();
+    await eventsPage.waitForEventsTable();
+
+    // Verify filter bar (containing search functionality) is visible
+    await expect(eventsPage.filterBar).toBeVisible({ timeout: 10_000 });
+
+    // Verify the filter button is present (allows adding search filters)
+    const filterButton = eventsPage.page.locator('button:has-text("Filter")');
+
+    await expect(filterButton).toBeVisible();
+
+    // Click the filter button to open the filter menu
+    await filterButton.click();
+
+    // Verify filter menu opened - be more specific to target the filter menu
+    const filterMenu = 
eventsPage.page.locator('[role="menu"][aria-labelledby*="menu"][data-state="open"]');
+
+    await expect(filterMenu).toBeVisible({ timeout: 5000 });
+
+    // Look for text search options in the menu
+    const textSearchOptions = eventsPage.page.locator(
+      '[role="menuitem"]:has-text("DAG ID"), [role="menuitem"]:has-text("Event 
Type"), [role="menuitem"]:has-text("User")',
+    );
+
+    const textSearchOptionsCount = await textSearchOptions.count();
+
+    expect(textSearchOptionsCount).toBeGreaterThan(0);
+    await expect(textSearchOptions.first()).toBeVisible();
+
+    // Close the menu by pressing Escape
+    await eventsPage.page.keyboard.press("Escape");
+  });
+});
+
+test.describe("Events with Generated Data", () => {
+  let eventsPage: EventsPage;
+  const testDagId = testConfig.testDag.id;
+
+  test.setTimeout(60_000);
+
+  test.beforeAll(async ({ browser }) => {
+    test.setTimeout(3 * 60 * 1000);
+    const context = await browser.newContext({ storageState: AUTH_FILE });
+    const page = await context.newPage();
+    const dagsPage = new DagsPage(page);
+
+    await dagsPage.triggerDag(testDagId);
+    await context.close();
+  });
+
+  test.beforeEach(({ page }) => {
+    eventsPage = new EventsPage(page);
+  });
+
+  test("verify audit log entries display valid data", async () => {
+    await eventsPage.navigate();
+
+    await expect(eventsPage.eventsTable).toBeVisible();
+
+    const rowCount = await eventsPage.getTableRowCount();
+
+    expect(rowCount).toBeGreaterThan(0);
+    await eventsPage.verifyLogEntriesWithData();
+  });
+
+  test("verify search for specific event type and filtered results", async () 
=> {
+    await eventsPage.navigate();
+
+    const initialRowCount = await eventsPage.getTableRowCount();
+
+    expect(initialRowCount).toBeGreaterThan(0);
+
+    await eventsPage.addFilter("Event Type");
+    await eventsPage.setFilterValue("Event Type", "cli");
+    await expect(eventsPage.eventsTable).toBeVisible();
+
+    await expect(async () => {
+      const filteredEvents = await eventsPage.getEventTypes(false);
+
+      expect(filteredEvents.length).toBeGreaterThan(0);
+      for (const event of filteredEvents) {
+        expect(event.toLowerCase()).toContain("cli");
+      }
+    }).toPass({ timeout: 20_000 });
+  });
+
+  test("verify filter by DAG ID", async () => {
+    await eventsPage.navigate();
+    await eventsPage.addFilter("DAG ID");
+    await eventsPage.setFilterValue("DAG ID", testDagId);
+    await expect(eventsPage.eventsTable).toBeVisible();
+
+    await expect(async () => {
+      const rows = await eventsPage.getEventLogRows();
+
+      expect(rows.length).toBeGreaterThan(0);
+
+      await expect(eventsPage.eventsTable).toBeVisible();
+
+      for (const row of rows) {
+        const dagIdCell = await eventsPage.getCellByColumnName(row, "DAG ID");
+        const dagIdText = await dagIdCell.textContent();
+
+        expect(dagIdText?.toLowerCase()).toContain(testDagId.toLowerCase());
+      }
+    }).toPass({ timeout: 20_000 });
+  });
+});

Reply via email to