This is an automated email from the ASF dual-hosted git repository.
choo121600 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 6c97aede3f0 improve e2e tests for dag audit log (#63463)
6c97aede3f0 is described below
commit 6c97aede3f0e10f40dc6311ac0af26cc4b008b5e
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+/);
});
});