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