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 });
+ });
+});