From daf71700be48601694e2a1b97a6fcc1267bbfb18 Mon Sep 17 00:00:00 2001 From: Harsh Thakur <73895659+iharsh02@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:21:44 +0530 Subject: [PATCH 1/4] Remove pagination and sorting tests --- .../ui/tests/e2e/pages/ConfigurationPage.ts | 85 ++++++++++++++ .../airflow/ui/tests/e2e/pages/DagRunsPage.ts | 43 ------- .../ui/tests/e2e/pages/DagRunsTabPage.ts | 78 ------------- .../airflow/ui/tests/e2e/pages/DagsPage.ts | 76 ------------ .../airflow/ui/tests/e2e/pages/EventsPage.ts | 108 +----------------- .../ui/tests/e2e/pages/ProvidersPage.ts | 34 +----- .../ui/tests/e2e/pages/RequiredActionsPage.ts | 67 +---------- .../ui/tests/e2e/pages/TaskInstancesPage.ts | 46 -------- .../airflow/ui/tests/e2e/pages/XComsPage.ts | 40 ------- .../ui/tests/e2e/specs/Configuration.spec.ts | 46 ++++++++ .../airflow/ui/tests/e2e/specs/asset.spec.ts | 20 ---- .../ui/tests/e2e/specs/dag-audit-log.spec.ts | 38 ------ .../ui/tests/e2e/specs/dag-runs-tab.spec.ts | 20 ---- .../ui/tests/e2e/specs/dag-runs.spec.ts | 4 - .../ui/tests/e2e/specs/dags-list.spec.ts | 85 -------------- .../ui/tests/e2e/specs/events-page.spec.ts | 2 +- .../ui/tests/e2e/specs/providers.spec.ts | 51 --------- .../ui/tests/e2e/specs/requiredAction.spec.ts | 6 - .../ui/tests/e2e/specs/task-instances.spec.ts | 24 ---- .../airflow/ui/tests/e2e/specs/xcoms.spec.ts | 5 - .../commands/testing_commands.py | 1 + 21 files changed, 140 insertions(+), 739 deletions(-) create mode 100644 airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts create mode 100644 airflow-core/src/airflow/ui/tests/e2e/specs/Configuration.spec.ts diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts new file mode 100644 index 0000000000000..0e393040fd8df --- /dev/null +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts @@ -0,0 +1,85 @@ +/*! + * 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 type { Locator, Page } from "@playwright/test"; + +import { BasePage } from "./BasePage"; + +export class ConfigurationPage extends BasePage { + public readonly heading: Locator; + public readonly rows: Locator; + public readonly table: Locator; + + public constructor(page: Page) { + super(page); + + this.heading = page.getByRole("heading", { + name: /config/i, + }); + this.table = page.getByTestId("table-list"); + this.rows = this.table.locator("tbody tr").filter({ + has: page.locator("td"), + }); + } + + public async getRowCount(): Promise { + return this.rows.count(); + } + + public async getRowDetails(index: number) { + const row = this.rows.nth(index); + const cells = row.locator("td"); + + const section = await cells.nth(0).textContent(); + const key = await cells.nth(1).textContent(); + const value = await cells.nth(2).textContent(); + + return { + key: (key ?? "").trim(), + section: (section ?? "").trim(), + value: (value ?? "").trim(), + }; + } + + public async navigate(): Promise { + await this.navigateTo("/configs"); + } + + public async waitForLoad(): Promise { + await this.table.waitFor({ state: "visible", timeout: 30_000 }); + await this.waitForTableData(); + } + + private async waitForTableData(): Promise { + await this.page.waitForFunction( + () => { + const table = document.querySelector('[data-testid="table-list"]'); + + if (!table) { + return false; + } + + const cells = table.querySelectorAll("tbody tr td"); + + return cells.length > 0; + }, + undefined, + { timeout: 30_000 }, + ); + } +} diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsPage.ts index 080def6218d47..8ace70b132edf 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsPage.ts @@ -82,49 +82,6 @@ export class DagRunsPage extends BasePage { expect(await dataLinks.count()).toBeGreaterThan(0); } - /** - * Verify pagination controls and navigation - */ - public async verifyPagination(limit: number): Promise { - await this.navigateTo(`${DagRunsPage.dagRunsUrl}?offset=0&limit=${limit}`); - await this.page.waitForURL(/.*limit=/, { timeout: 10_000 }); - await this.page.waitForLoadState("networkidle"); - await this.dagRunsTable.waitFor({ state: "visible", timeout: 10_000 }); - - const dataLinks = this.dagRunsTable.locator("a[href*='/dags/']"); - - await expect(dataLinks.first()).toBeVisible({ timeout: 30_000 }); - - const rows = this.dagRunsTable.locator("tbody tr"); - - expect(await rows.count()).toBeGreaterThan(0); - - const paginationNav = this.page.locator('nav[aria-label="pagination"], [role="navigation"]'); - - await expect(paginationNav.first()).toBeVisible({ timeout: 10_000 }); - - const page1Button = this.page.getByRole("button", { name: /page 1|^1$/ }); - - await expect(page1Button.first()).toBeVisible({ timeout: 5000 }); - - const page2Button = this.page.getByRole("button", { name: /page 2|^2$/ }); - const hasPage2 = await page2Button - .first() - .isVisible() - .catch(() => false); - - if (hasPage2) { - await page2Button.first().click(); - await this.page.waitForLoadState("networkidle"); - await this.dagRunsTable.waitFor({ state: "visible", timeout: 10_000 }); - - const dataLinksPage2 = this.dagRunsTable.locator("a[href*='/dags/']"); - const noDataMessage = this.page.locator("text=/no.*data|no.*runs|no.*results/i"); - - await expect(dataLinksPage2.first().or(noDataMessage.first())).toBeVisible({ timeout: 30_000 }); - } - } - /** * Verify that run details are displayed in the table row */ diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts index 1e5a3e0c9433a..f89130f0a204e 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts @@ -21,57 +21,18 @@ import { BasePage } from "tests/e2e/pages/BasePage"; export class DagRunsTabPage extends BasePage { public readonly markRunAsButton: Locator; - public readonly nextPageButton: Locator; - public readonly prevPageButton: Locator; public readonly runsTable: Locator; public readonly tableRows: Locator; public readonly triggerButton: Locator; - private currentDagId?: string; - private currentLimit?: number; - public constructor(page: Page) { super(page); this.markRunAsButton = page.locator('[data-testid="mark-run-as-button"]').first(); - this.nextPageButton = page.locator('[data-testid="next"]'); - this.prevPageButton = page.locator('[data-testid="prev"]'); this.runsTable = page.locator('[data-testid="table-list"]'); this.tableRows = this.runsTable.locator("tbody tr"); this.triggerButton = page.locator('[data-testid="trigger-dag-button"]'); } - public async clickNextPage(): Promise { - await this.waitForRunsTableToLoad(); - const firstRunLink = this.tableRows.first().locator("a[href*='/runs/']").first(); - - await expect(firstRunLink).toBeVisible(); - const firstRunId = await firstRunLink.textContent(); - - if (firstRunId === null || firstRunId === "") { - throw new Error("Could not get first run ID before pagination"); - } - - await this.nextPageButton.click(); - await expect(this.tableRows.first()).not.toContainText(firstRunId, { timeout: 10_000 }); - await this.ensureUrlParams(); - } - - public async clickPrevPage(): Promise { - await this.waitForRunsTableToLoad(); - const firstRunLink = this.tableRows.first().locator("a[href*='/runs/']").first(); - - await expect(firstRunLink).toBeVisible(); - const firstRunId = await firstRunLink.textContent(); - - if (firstRunId === null || firstRunId === "") { - throw new Error("Could not get first run ID before pagination"); - } - - await this.prevPageButton.click(); - await expect(this.tableRows.first()).not.toContainText(firstRunId, { timeout: 10_000 }); - await this.ensureUrlParams(); - } - public async clickRunAndVerifyDetails(): Promise { const firstRunLink = this.tableRows.first().locator("a[href*='/runs/']").first(); @@ -90,17 +51,6 @@ export class DagRunsTabPage extends BasePage { await this.waitForRunsTableToLoad(); } - public async clickRunsTabWithPageSize(dagId: string, pageSize: number): Promise { - this.currentDagId = dagId; - this.currentLimit = pageSize; - - await this.navigateTo(`/dags/${dagId}/runs?offset=0&limit=${pageSize}`); - await this.page.waitForURL(/.*\/dags\/[^/]+\/runs.*offset=0&limit=/, { - timeout: 15_000, - }); - await this.waitForRunsTableToLoad(); - } - public async filterByState(state: string): Promise { const currentUrl = new URL(this.page.url()); @@ -110,12 +60,6 @@ export class DagRunsTabPage extends BasePage { await this.waitForRunsTableToLoad(); } - public async getRowCount(): Promise { - await this.waitForRunsTableToLoad(); - - return this.tableRows.count(); - } - public async markRunAs(state: "failed" | "success"): Promise { const stateBadge = this.page.locator('[data-testid="state-badge"]').first(); @@ -269,26 +213,4 @@ export class DagRunsTabPage extends BasePage { await expect(dataLink.or(noDataMessage)).toBeVisible({ timeout: 30_000 }); } - - private async ensureUrlParams(): Promise { - if (this.currentLimit === undefined || this.currentDagId === undefined) { - return; - } - - const currentUrl = this.page.url(); - const url = new URL(currentUrl); - const hasLimit = url.searchParams.has("limit"); - const hasOffset = url.searchParams.has("offset"); - - if (hasLimit && !hasOffset) { - url.searchParams.set("offset", "0"); - await this.navigateTo(url.pathname + url.search); - await this.waitForRunsTableToLoad(); - } else if (!hasLimit && !hasOffset) { - url.searchParams.set("offset", "0"); - url.searchParams.set("limit", String(this.currentLimit)); - await this.navigateTo(url.pathname + url.search); - await this.waitForRunsTableToLoad(); - } - } } diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts index b3e65857339e2..5f510b797b129 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts @@ -35,14 +35,11 @@ export class DagsPage extends BasePage { public readonly failedFilter: Locator; public readonly needsReviewFilter: Locator; public readonly operatorFilter: Locator; - public readonly paginationNextButton: Locator; - public readonly paginationPrevButton: Locator; public readonly queuedFilter: Locator; public readonly retriesFilter: Locator; public readonly runningFilter: Locator; public readonly searchBox: Locator; public readonly searchInput: Locator; - public readonly sortSelect: Locator; public readonly stateElement: Locator; public readonly successFilter: Locator; public readonly tableViewButton: Locator; @@ -61,8 +58,6 @@ export class DagsPage extends BasePage { // page trigger button has visible text or is icon-only. this.confirmButton = page.locator('button:has-text("Trigger")').last(); this.stateElement = page.locator('*:has-text("State") + *').first(); - this.paginationNextButton = page.locator('[data-testid="next"]'); - this.paginationPrevButton = page.locator('[data-testid="prev"]'); this.searchBox = page.getByRole("textbox", { name: /search/i }); this.searchInput = page.getByPlaceholder("Search DAGs"); this.operatorFilter = page.getByRole("combobox").filter({ hasText: /operator/i }); @@ -71,8 +66,6 @@ export class DagsPage extends BasePage { // View toggle buttons this.cardViewButton = page.locator('button[aria-label="Show card view"]'); this.tableViewButton = page.locator('button[aria-label="Show table view"]'); - // Sort select (card view only) - this.sortSelect = page.locator('[data-testid="sort-by-select"]'); // Status filter buttons this.successFilter = page.locator('button:has-text("Success")'); this.failedFilter = page.locator('button:has-text("Failed")'); @@ -106,75 +99,6 @@ export class DagsPage extends BasePage { await this.waitForDagList(); } - /** - * Click next page button and wait for list to change - */ - public async clickNextPage(): Promise { - const initialDagNames = await this.getDagNames(); - - // Set up API listener before action - const responsePromise = this.page - .waitForResponse((resp) => resp.url().includes("/dags") && resp.status() === 200, { - timeout: 30_000, - }) - .catch(() => { - /* API might be cached */ - }); - - await this.paginationNextButton.click(); - - // Wait for API response - await responsePromise; - - // Wait for UI to actually change (increased timeout for slower browsers like Firefox) - await expect - .poll(() => this.getDagNames(), { - message: "List did not update after clicking next page", - timeout: 30_000, - }) - .not.toEqual(initialDagNames); - - await this.waitForDagList(); - } - - /** - * Click previous page button and wait for list to change - */ - public async clickPrevPage(): Promise { - const initialDagNames = await this.getDagNames(); - - // Set up API listener before action - const responsePromise = this.page - .waitForResponse((resp) => resp.url().includes("/dags") && resp.status() === 200, { - timeout: 30_000, - }) - .catch(() => { - /* API might be cached */ - }); - - await this.paginationPrevButton.click(); - - // Wait for API response - await responsePromise; - - // Wait for UI to actually change (increased timeout for slower browsers like Firefox) - await expect - .poll(() => this.getDagNames(), { - message: "List did not update after clicking prev page", - timeout: 30_000, - }) - .not.toEqual(initialDagNames); - - await this.waitForDagList(); - } - - /** - * Click sort select (only works in card view) - */ - public async clickSortSelect(): Promise { - await this.sortSelect.click(); - } - public async filterByOperator(operator: string): Promise { await this.selectDropdownOption(this.operatorFilter, operator); } 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 35a84242f6f2c..8c1766be09806 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/EventsPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/EventsPage.ts @@ -27,14 +27,9 @@ export class EventsPage extends BasePage { public readonly extraColumn: Locator; public readonly filterBar: Locator; public readonly ownerColumn: Locator; - public readonly paginationNextButton: Locator; - public readonly paginationPrevButton: Locator; public readonly tableRows: Locator; public readonly whenColumn: Locator; - private currentDagId?: string; - private currentLimit?: number; - public constructor(page: Page) { super(page); this.eventsPageTitle = page.locator('h2:has-text("Audit Log")'); @@ -46,8 +41,6 @@ export class EventsPage extends BasePage { .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"]'); this.tableRows = this.eventsTable.locator("tbody tr"); this.whenColumn = this.eventsTable.locator('th:has-text("When")'); } @@ -70,35 +63,6 @@ export class EventsPage extends BasePage { await menuItem.click(); } - public async clickColumnHeader(columnKey: string): Promise { - 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 { - const columnHeader = this.eventsTable.locator(`th:has-text("${columnName}")`); - const sortButton = columnHeader.locator('button[aria-label="sort"]'); - - await sortButton.click(); - await this.waitForTableLoad(); - await this.ensureUrlParams(); - } - - public async clickNextPage(): Promise { - await this.paginationNextButton.click(); - await this.waitForTableLoad(); - await this.ensureUrlParams(); - } - - public async clickPrevPage(): Promise { - await this.paginationPrevButton.click(); - await this.waitForTableLoad(); - await this.ensureUrlParams(); - } - public async getCellByColumnName(row: Locator, columnName: string): Promise { const headers = await this.eventsTable.locator("thead th").allTextContents(); const index = headers.findIndex((h) => h.toLowerCase().includes(columnName.toLowerCase())); @@ -120,7 +84,7 @@ export class EventsPage extends BasePage { return this.tableRows.all(); } - public async getEventTypes(allPages: boolean = false): Promise> { + public async getEventTypes(): Promise> { const rows = await this.getEventLogRows(); if (rows.length === 0) { @@ -138,24 +102,7 @@ export class EventsPage extends BasePage { } } - if (!allPages) { - return eventTypes; - } - - const allEventTypes = [...eventTypes]; - const startUrl = this.page.url(); - - while (await this.hasNextPage()) { - await this.clickNextPage(); - const pageEvents = await this.getEventTypes(false); - - allEventTypes.push(...pageEvents); - } - - await this.page.goto(startUrl, { timeout: 30_000, waitUntil: "domcontentloaded" }); - await this.waitForTableLoad(); - - return allEventTypes; + return eventTypes; } public getFilterPill(filterLabel: string): Locator { @@ -166,29 +113,13 @@ export class EventsPage extends BasePage { return this.tableRows.count(); } - public async hasNextPage(): Promise { - const count = await this.paginationNextButton.count(); - - if (count === 0) { - return false; - } - - return await this.paginationNextButton.isEnabled(); - } - public async navigate(): Promise { await this.navigateTo("/events"); await this.waitForTableLoad(); } - public async navigateToAuditLog(dagId: string, limit?: number): Promise { - this.currentDagId = dagId; - this.currentLimit = limit; - - const baseUrl = EventsPage.getEventsUrl(dagId); - const url = limit === undefined ? baseUrl : `${baseUrl}?offset=0&limit=${limit}`; - - await this.page.goto(url, { + public async navigateToAuditLog(dagId: string): Promise { + await this.page.goto(EventsPage.getEventsUrl(dagId), { timeout: 30_000, waitUntil: "domcontentloaded", }); @@ -296,35 +227,4 @@ export class EventsPage extends BasePage { { timeout: 60_000 }, ); } - - /** - * Ensure offset=0 is present when limit is set to prevent limit from being ignored - */ - private async ensureUrlParams(): Promise { - if (this.currentLimit === undefined || this.currentDagId === undefined) { - return; - } - - const currentUrl = this.page.url(); - const url = new URL(currentUrl); - const hasLimit = url.searchParams.has("limit"); - const hasOffset = url.searchParams.has("offset"); - - if (hasLimit && !hasOffset) { - url.searchParams.set("offset", "0"); - await this.page.goto(url.toString(), { - timeout: 30_000, - waitUntil: "domcontentloaded", - }); - await this.waitForTableLoad(); - } else if (!hasLimit && !hasOffset) { - url.searchParams.set("offset", "0"); - url.searchParams.set("limit", String(this.currentLimit)); - await this.page.goto(url.toString(), { - timeout: 30_000, - waitUntil: "domcontentloaded", - }); - await this.waitForTableLoad(); - } - } } diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts index 6b597938f3358..368b8302f8b6e 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts @@ -16,14 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { expect, type Locator, type Page } from "@playwright/test"; +import type { Locator, Page } from "@playwright/test"; import { BasePage } from "./BasePage"; export class ProvidersPage extends BasePage { public readonly heading: Locator; - public readonly paginationNextButton: Locator; - public readonly paginationPrevButton: Locator; public readonly rows: Locator; public readonly table: Locator; @@ -35,32 +33,6 @@ export class ProvidersPage extends BasePage { this.rows = this.table.locator("tbody tr").filter({ has: page.locator("td"), }); - this.paginationNextButton = page.locator('[data-testid="next"]'); - this.paginationPrevButton = page.locator('[data-testid="prev"]'); - } - - /** - * Click next page button - */ - public async clickNextPage(): Promise { - const initialProviderNames = await this.providerNames(); - - await this.paginationNextButton.click(); - - await expect.poll(() => this.providerNames(), { timeout: 10_000 }).not.toEqual(initialProviderNames); - await this.waitForTableData(); - } - - /** - * Click previous page button - */ - public async clickPrevPage(): Promise { - const initialProviderNames = await this.providerNames(); - - await this.paginationPrevButton.click(); - - await expect.poll(() => this.providerNames(), { timeout: 10_000 }).not.toEqual(initialProviderNames); - await this.waitForTableData(); } public async getRowCount(): Promise { @@ -86,10 +58,6 @@ export class ProvidersPage extends BasePage { await this.navigateTo("/providers"); } - public async providerNames(): Promise> { - return this.rows.locator("td a").allTextContents(); - } - public async waitForLoad(): Promise { await this.table.waitFor({ state: "visible", timeout: 30_000 }); await this.waitForTableData(); diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts index 6c4b86fcd14a8..9381cc69d259c 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts @@ -25,32 +25,18 @@ export class RequiredActionsPage extends BasePage { public readonly actionsTable: Locator; public readonly emptyStateMessage: Locator; public readonly pageHeading: Locator; - public readonly paginationNextButton: Locator; - public readonly paginationPrevButton: Locator; public constructor(page: Page) { super(page); this.pageHeading = page.getByRole("heading").filter({ hasText: /required action/i }); this.actionsTable = page.getByTestId("table-list"); this.emptyStateMessage = page.getByText(/no required actions found/i); - this.paginationNextButton = page.locator('[data-testid="next"]'); - this.paginationPrevButton = page.locator('[data-testid="prev"]'); } public static getRequiredActionsUrl(): string { return "/required_actions"; } - public async clickNextPage(): Promise { - await this.paginationNextButton.click(); - await expect(this.actionsTable).toBeVisible({ timeout: 10_000 }); - } - - public async clickPrevPage(): Promise { - await this.paginationPrevButton.click(); - await expect(this.actionsTable).toBeVisible({ timeout: 10_000 }); - } - public async getActionsTableRowCount(): Promise { const rows = this.page.locator("table tbody tr"); const isTableVisible = await this.actionsTable.isVisible(); @@ -62,35 +48,16 @@ export class RequiredActionsPage extends BasePage { return rows.count(); } - public async getActionSubjects(): Promise> { - const rows = this.page.locator("table tbody tr td:nth-child(2)"); - const texts = await rows.allTextContents(); - - return texts.map((text) => text.trim()).filter((text) => text !== ""); - } - - public async hasNextPage(): Promise { - const isDisabled = await this.paginationNextButton.isDisabled(); - - return !isDisabled; - } - public async isEmptyStateDisplayed(): Promise { return this.emptyStateMessage.isVisible(); } - public async isPaginationVisible(): Promise { - return this.paginationNextButton.isVisible(); - } - public async isTableDisplayed(): Promise { return this.actionsTable.isVisible(); } - public async navigateToRequiredActionsPage(limit?: number): Promise { - await (limit === undefined - ? this.navigateTo(RequiredActionsPage.getRequiredActionsUrl()) - : this.navigateTo(`${RequiredActionsPage.getRequiredActionsUrl()}?limit=${limit}&offset=0`)); + public async navigateToRequiredActionsPage(): Promise { + await this.navigateTo(RequiredActionsPage.getRequiredActionsUrl()); await expect(this.pageHeading).toBeVisible({ timeout: 10_000 }); } @@ -102,36 +69,6 @@ export class RequiredActionsPage extends BasePage { await this.runHITLFlow(dagId, false); } - public async verifyPagination(limit: number): Promise { - await this.navigateToRequiredActionsPage(limit); - - await expect(this.paginationNextButton).toBeVisible(); - await expect(this.paginationPrevButton).toBeVisible(); - - const page1Actions = await this.getActionSubjects(); - - expect(page1Actions.length).toBeGreaterThan(0); - - await this.clickNextPage(); - - await expect - .poll( - async () => { - const subjects = await this.getActionSubjects(); - - return subjects.length > 0 && subjects.join(",") !== page1Actions.join(","); - }, - { timeout: 30_000 }, - ) - .toBe(true); - - const page2Actions = await this.getActionSubjects(); - - await this.clickPrevPage(); - - await expect.poll(() => this.getActionSubjects(), { timeout: 30_000 }).not.toEqual(page2Actions); - } - private async clickButtonAndWaitForHITLResponse(button: Locator): Promise { const responsePromise = this.page.waitForResponse( (res) => res.url().includes("hitlDetails") && res.request().method() === "PATCH", diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/TaskInstancesPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/TaskInstancesPage.ts index 9c560888ec19a..87506271fad50 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/TaskInstancesPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/TaskInstancesPage.ts @@ -24,59 +24,13 @@ export class TaskInstancesPage extends BasePage { return "/task_instances"; } - public readonly paginationNextButton: Locator; - - public readonly paginationPrevButton: Locator; - public readonly taskInstancesTable: Locator; public constructor(page: Page) { super(page); - this.paginationNextButton = page.locator('[data-testid="next"]'); - this.paginationPrevButton = page.locator('[data-testid="prev"]'); this.taskInstancesTable = page.locator('table, div[role="table"]'); } - /** - * Click next page button - */ - public async clickNextPage(): Promise { - const initialTaskInstanceIds = await this.getTaskInstanceIds(); - - await this.paginationNextButton.click(); - - await expect - .poll(() => this.getTaskInstanceIds(), { timeout: 10_000 }) - .not.toEqual(initialTaskInstanceIds); - - await this.waitForTaskInstanceList(); - } - - /** - * Click previous page button - */ - public async clickPrevPage(): Promise { - const initialTaskInstanceIds = await this.getTaskInstanceIds(); - - await this.paginationPrevButton.click(); - - await expect - .poll(() => this.getTaskInstanceIds(), { timeout: 10_000 }) - .not.toEqual(initialTaskInstanceIds); - await this.waitForTaskInstanceList(); - } - - /** - * Get all task instance identifiers from the current page - */ - public async getTaskInstanceIds(): Promise> { - await this.waitForTaskInstanceList(); - const taskLinks = this.taskInstancesTable.locator("a[href*='/dags/']"); - const texts = await taskLinks.allTextContents(); - - return texts.map((text) => text.trim()).filter((text) => text !== ""); - } - /** * Navigate to Task Instances page and wait for data to load */ diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/XComsPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/XComsPage.ts index d28e34b2cd788..188a5d81fd908 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/XComsPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/XComsPage.ts @@ -27,8 +27,6 @@ export class XComsPage extends BasePage { public readonly addFilterButton: Locator; public readonly collapseAllButton: Locator; public readonly expandAllButton: Locator; - public readonly paginationNextButton: Locator; - public readonly paginationPrevButton: Locator; public readonly xcomsTable: Locator; public constructor(page: Page) { @@ -36,8 +34,6 @@ export class XComsPage extends BasePage { this.addFilterButton = page.locator('[data-testid="add-filter-button"]'); this.collapseAllButton = page.locator('[data-testid="collapse-all-button"]'); this.expandAllButton = page.locator('[data-testid="expand-all-button"]'); - this.paginationNextButton = page.locator('[data-testid="next"]'); - this.paginationPrevButton = page.locator('[data-testid="prev"]'); this.xcomsTable = page.locator('[data-testid="table-list"]'); } @@ -127,42 +123,6 @@ export class XComsPage extends BasePage { } } - public async verifyPagination(limit: number): Promise { - await this.navigateTo(`${XComsPage.xcomsUrl}?offset=0&limit=${limit}`); - await this.page.waitForURL(/.*offset=0.*limit=/, { timeout: 10_000 }); - await this.page.waitForLoadState("networkidle"); - await this.xcomsTable.waitFor({ state: "visible", timeout: 10_000 }); - - const rows = this.xcomsTable.locator("tbody tr"); - - expect(await rows.count()).toBeGreaterThan(0); - - await expect(this.paginationNextButton).toBeVisible({ timeout: 10_000 }); - await this.paginationNextButton.click(); - await this.page.waitForLoadState("networkidle"); - await this.xcomsTable.waitFor({ state: "visible", timeout: 10_000 }); - - const urlPage2 = this.page.url(); - - expect(urlPage2).toContain(`limit=${limit}`); - expect(urlPage2).not.toContain("offset=0&"); - - const rows2 = this.xcomsTable.locator("tbody tr"); - - expect(await rows2.count()).toBeGreaterThan(0); - - await expect(this.paginationPrevButton).toBeVisible({ timeout: 5000 }); - await this.paginationPrevButton.click(); - await this.page.waitForLoadState("networkidle"); - await this.xcomsTable.waitFor({ state: "visible", timeout: 10_000 }); - - const urlBackToPage1 = this.page.url(); - - expect(urlBackToPage1).toContain(`limit=${limit}`); - const isPage1 = urlBackToPage1.includes("offset=0") || !urlBackToPage1.includes("offset="); - - expect(isPage1).toBeTruthy(); - } public async verifyXComDetailsDisplay(): Promise { const firstRow = this.xcomsTable.locator("tbody tr").first(); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/Configuration.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/Configuration.spec.ts new file mode 100644 index 0000000000000..ea75fe21f4c4a --- /dev/null +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/Configuration.spec.ts @@ -0,0 +1,46 @@ +/*! + * 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 { ConfigurationPage } from "../pages/ConfigurationPage"; + +test.describe("Configuration Page", () => { + let configPage: ConfigurationPage; + + test.beforeEach(async ({ page }) => { + configPage = new ConfigurationPage(page); + await configPage.navigate(); + await configPage.waitForLoad(); + }); + + test("verify configuration displays", async () => { + await expect(configPage.heading).toBeVisible(); + await expect(configPage.table).toBeVisible(); + + const count = await configPage.getRowCount(); + + expect(count).toBeGreaterThan(0); + + const { key, section, value } = await configPage.getRowDetails(0); + + expect(section.length).toBeGreaterThan(0); + expect(key.length).toBeGreaterThan(0); + expect(value.length).toBeGreaterThan(0); + }); +}); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts index a123b64b101b7..be99eebab23a0 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts @@ -117,26 +117,6 @@ test.describe("Assets Page", () => { } }); - test("verify pagination controls navigate between pages", async () => { - await assets.navigateTo("/assets?limit=5&offset=0"); - await assets.waitForLoad(); - - const page1Initial = await assets.assetNames(); - - expect(page1Initial.length).toBeGreaterThan(0); - - const pagination = assets.page.locator('[data-scope="pagination"]'); - - await pagination.getByRole("button", { name: /page 2/i }).click(); - await expect.poll(() => assets.assetNames(), { timeout: 30_000 }).not.toEqual(page1Initial); - - const page2Assets = await assets.assetNames(); - - await pagination.getByRole("button", { name: /page 1/i }).click(); - - await expect.poll(() => assets.assetNames(), { timeout: 30_000 }).not.toEqual(page2Assets); - }); - test("verify asset details and dependencies", async ({ page }) => { const assetDetailPage = new AssetDetailPage(page); const assetName = testConfig.asset.name; 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 ebbfdb6b7193d..6b6989ad1a8c1 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 @@ -117,42 +117,4 @@ test.describe("DAG Audit Log", () => { expect(userText).toBeTruthy(); expect(userText?.trim()).not.toBe(""); }); - - test("verify pagination through audit log entries", async () => { - await eventsPage.navigateToAuditLog(testDagId, 3); - - const hasNext = await eventsPage.hasNextPage(); - - expect(hasNext).toBe(true); - - const urlPage1 = eventsPage.page.url(); - - expect(urlPage1).toContain("offset=0"); - expect(urlPage1).toContain("limit=3"); - - await eventsPage.clickNextPage(); - - const urlPage2 = eventsPage.page.url(); - - expect(urlPage2).toContain("limit=3"); - expect(urlPage2).not.toContain("offset=0"); - - await eventsPage.clickPrevPage(); - - const urlBackToPage1 = eventsPage.page.url(); - - expect(urlBackToPage1).toContain("offset=0"); - expect(urlBackToPage1).toContain("limit=3"); - }); - - test("verify sorting when clicking column header", async () => { - await eventsPage.navigateToAuditLog(testDagId); - - await eventsPage.clickColumnToSort("Event"); - - const sortedEvents = await eventsPage.getEventTypes(true); - - expect(sortedEvents.length).toBeGreaterThan(0); - expect(sortedEvents).toEqual([...sortedEvents].sort()); - }); }); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts index 14dcd6e3e2adf..d9aef74f5476b 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts @@ -100,24 +100,4 @@ test.describe("DAG Runs Tab", () => { await dagRunsTabPage.searchByRunIdPattern("manual"); await dagRunsTabPage.verifySearchResults("manual"); }); - - test("paginate through runs", async () => { - await dagRunsTabPage.clickRunsTabWithPageSize(testDagId, 2); - - const initialRowCount = await dagRunsTabPage.getRowCount(); - - expect(initialRowCount).toBe(2); - - await dagRunsTabPage.clickNextPage(); - - const nextPageRowCount = await dagRunsTabPage.getRowCount(); - - expect(nextPageRowCount).toBeGreaterThan(0); - - await dagRunsTabPage.clickPrevPage(); - - const backRowCount = await dagRunsTabPage.getRowCount(); - - expect(backRowCount).toBe(initialRowCount); - }); }); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts index 718f00b7cd0d6..5567ba6d58b44 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts @@ -139,8 +139,4 @@ test.describe("DAG Runs Page", () => { await dagRunsPage.navigate(); await dagRunsPage.verifyDagIdFiltering(testDagId1); }); - - test("verify pagination with offset and limit", async () => { - await dagRunsPage.verifyPagination(3); - }); }); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts index 193c2af6652f2..6d0a257757cfd 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts @@ -20,39 +20,6 @@ import { expect, test } from "@playwright/test"; import { testConfig } from "playwright.config"; import { DagsPage } from "tests/e2e/pages/DagsPage"; -test.describe("Dags Pagination", () => { - let dagsPage: DagsPage; - - test.beforeEach(({ page }) => { - dagsPage = new DagsPage(page); - }); - - test("should verify pagination works on the Dags list page", async () => { - test.setTimeout(120_000); // 2 minutes for slower browsers like Firefox - await dagsPage.navigate(); - - await expect(dagsPage.paginationNextButton).toBeVisible(); - await expect(dagsPage.paginationPrevButton).toBeVisible(); - - const initialDagNames = await dagsPage.getDagNames(); - - expect(initialDagNames.length).toBeGreaterThan(0); - - await dagsPage.clickNextPage(); - - const dagNamesAfterNext = await dagsPage.getDagNames(); - - expect(dagNamesAfterNext.length).toBeGreaterThan(0); - expect(dagNamesAfterNext).not.toEqual(initialDagNames); - - await dagsPage.clickPrevPage(); - - const dagNamesAfterPrev = await dagsPage.getDagNames(); - - expect(dagNamesAfterPrev).toEqual(initialDagNames); - }); -}); - test.describe("Dag Trigger Workflow", () => { let dagsPage: DagsPage; const testDagId = testConfig.testDag.id; @@ -228,55 +195,3 @@ test.describe("Dags Status Filtering", () => { await dagsPage.verifyDagsListVisible(); }); }); - -test.describe("Dags Sorting", () => { - let dagsPage: DagsPage; - - test.beforeEach(({ page }) => { - dagsPage = new DagsPage(page); - }); - - test("should sort Dags by name in card view", async () => { - test.setTimeout(120_000); // 2 minutes for slower browsers like Firefox - await dagsPage.navigate(); - await dagsPage.verifyDagsListVisible(); - - await dagsPage.switchToCardView(); - - await expect(dagsPage.sortSelect).toBeVisible(); - - const ascNames = await dagsPage.getDagNames(); - - expect(ascNames.length).toBeGreaterThan(1); - - await dagsPage.clickSortSelect(); - - await expect(dagsPage.page.getByRole("option").first()).toBeVisible(); - - await dagsPage.page.getByRole("option", { name: "Sort by Display Name (Z-A)" }).click(); - - // Poll until the list order actually changes instead of a fixed delay - await expect - .poll(async () => dagsPage.getDagNames(), { - message: "List did not re-sort within timeout", - timeout: 10_000, - }) - .not.toEqual(ascNames); - - const descNames = await dagsPage.getDagNames(); - - expect(descNames.length).toBeGreaterThan(1); - - const [firstName] = descNames; - const lastName = descNames[descNames.length - 1]; - - expect(firstName).toBeDefined(); - expect(lastName).toBeDefined(); - - expect(firstName).not.toEqual(ascNames[0]); - - if (firstName !== undefined && firstName !== "" && lastName !== undefined && lastName !== "") { - expect(firstName.localeCompare(lastName)).toBeGreaterThan(0); - } - }); -}); 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 index bd2ae48b677d9..d8638e7026a75 100644 --- 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 @@ -114,7 +114,7 @@ test.describe("Events with Generated Data", () => { await expect(eventsPage.eventsTable).toBeVisible(); await expect(async () => { - const filteredEvents = await eventsPage.getEventTypes(false); + const filteredEvents = await eventsPage.getEventTypes(); expect(filteredEvents.length).toBeGreaterThan(0); for (const event of filteredEvents) { diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts index dc20c518d3ec5..526876d15e342 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts @@ -67,55 +67,4 @@ test.describe("Providers Page", () => { expect(description).not.toEqual(""); } }); - - test("verify providers pagination", async () => { - const limit = 5; - - await providers.navigateTo(`/providers?offset=0&limit=${limit}`); - await providers.waitForLoad(); - - const rows = await providers.getRowCount(); - - expect(rows).toBeGreaterThan(0); - - const initialProviderNames = await providers.providerNames(); - - expect(initialProviderNames.length).toBeGreaterThan(0); - - await expect(providers.paginationNextButton).toBeVisible(); - await expect(providers.paginationPrevButton).toBeVisible(); - - await providers.paginationNextButton.click(); - await providers.waitForLoad(); - - await providers.page.waitForURL((url) => { - const u = new URL(url); - const offset = u.searchParams.get("offset"); - - return offset !== null && offset !== "0"; - }); - - const rowsPage2 = await providers.getRowCount(); - - expect(rowsPage2).toBeGreaterThan(0); - - const ProviderNamesAfterNext = await providers.providerNames(); - - expect(ProviderNamesAfterNext.length).toBeGreaterThan(0); - expect(ProviderNamesAfterNext).not.toEqual(initialProviderNames); - - await providers.paginationPrevButton.click(); - await providers.waitForLoad(); - - await providers.page.waitForURL((url) => { - const u = new URL(url); - const offset = u.searchParams.get("offset"); - - return offset === "0" || offset === null; - }); - - const rowsBack = await providers.getRowCount(); - - expect(rowsBack).toBeGreaterThan(0); - }); }); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/requiredAction.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/requiredAction.spec.ts index 0b40d0d66fcda..976413e92cd16 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/requiredAction.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/requiredAction.spec.ts @@ -68,10 +68,4 @@ test.describe("Verify Required Action page", () => { await expect(browsePage.emptyStateMessage).toBeVisible(); } }); - - test("verify pagination with offset and limit", async ({ page }) => { - const browsePage = new RequiredActionsPage(page); - - await browsePage.verifyPagination(3); - }); }); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/task-instances.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/task-instances.spec.ts index 12c05aba0571e..aa8910ed85a8f 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/task-instances.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/task-instances.spec.ts @@ -136,28 +136,4 @@ test.describe("Task Instances Page", () => { await taskInstancesPage.navigate(); await taskInstancesPage.verifyStateFiltering("Success"); }); - - test("verify pagination with offset and limit", async () => { - await taskInstancesPage.navigate(); - - await expect(taskInstancesPage.paginationNextButton).toBeVisible(); - await expect(taskInstancesPage.paginationPrevButton).toBeVisible(); - - const initialTaskInstanceIds = await taskInstancesPage.getTaskInstanceIds(); - - expect(initialTaskInstanceIds.length).toBeGreaterThan(0); - - await taskInstancesPage.clickNextPage(); - - const taskInstanceIdsAfterNext = await taskInstancesPage.getTaskInstanceIds(); - - expect(taskInstanceIdsAfterNext.length).toBeGreaterThan(0); - expect(taskInstanceIdsAfterNext).not.toEqual(initialTaskInstanceIds); - - await taskInstancesPage.clickPrevPage(); - - const taskInstanceIdsAfterPrev = await taskInstancesPage.getTaskInstanceIds(); - - expect(taskInstanceIdsAfterPrev).toEqual(initialTaskInstanceIds); - }); }); diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/xcoms.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/xcoms.spec.ts index dcbe8a438358f..3911ab27c8af5 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/xcoms.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/xcoms.spec.ts @@ -27,7 +27,6 @@ test.describe("XComs Page", () => { let xcomsPage: XComsPage; const testDagId = testConfig.xcomDag.id; const testXComKey = "return_value"; - const paginationLimit = 3; const triggerCount = 2; test.beforeAll(async ({ browser }) => { @@ -97,8 +96,4 @@ test.describe("XComs Page", () => { test("verify filtering by DAG display name", async () => { await xcomsPage.verifyDagDisplayNameFiltering(testDagId); }); - - test("verify pagination works", async () => { - await xcomsPage.verifyPagination(paginationLimit); - }); }); diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands.py b/dev/breeze/src/airflow_breeze/commands/testing_commands.py index bd878e90b108d..c8cad6db6c313 100644 --- a/dev/breeze/src/airflow_breeze/commands/testing_commands.py +++ b/dev/breeze/src/airflow_breeze/commands/testing_commands.py @@ -1518,6 +1518,7 @@ def ui_e2e_tests( env_vars = { "AIRFLOW_UID": str(os.getuid()), "AIRFLOW__CORE__LOAD_EXAMPLES": "true", + "AIRFLOW__API__EXPOSE_CONFIG": "true", "AIRFLOW_IMAGE_NAME": image_name, } From 41001c5657fdc197f69c4f650713e4fc69700622 Mon Sep 17 00:00:00 2001 From: vatsrahul1001 Date: Wed, 25 Feb 2026 17:15:53 +0530 Subject: [PATCH 2/4] restore ConnectionsPage --- .../ui/tests/e2e/pages/ConnectionsPage.ts | 493 ++++++++++++++++++ 1 file changed, 493 insertions(+) create mode 100644 airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts new file mode 100644 index 0000000000000..ebad0e1d2cf41 --- /dev/null +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts @@ -0,0 +1,493 @@ +/*! + * 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, type Locator, type Page } from "@playwright/test"; +import { BasePage } from "tests/e2e/pages/BasePage"; + +type ConnectionDetails = { + conn_type: string; + connection_id: string; + description?: string; + extra?: string; + host?: string; + login?: string; + password?: string; + port?: number | string; + schema?: string; +}; + +export class ConnectionsPage extends BasePage { + // Page URLs + public static get connectionsListUrl(): string { + return "/connections"; + } + + public readonly addButton: Locator; + public readonly confirmDeleteButton: Locator; + public readonly connectionForm: Locator; + public readonly connectionIdHeader: Locator; + public readonly connectionIdInput: Locator; + // Core page elements + public readonly connectionsTable: Locator; + public readonly connectionTypeHeader: Locator; + public readonly connectionTypeSelect: Locator; + public readonly descriptionInput: Locator; + public readonly emptyState: Locator; + public readonly hostHeader: Locator; + public readonly hostInput: Locator; + public readonly loginInput: Locator; + public readonly passwordInput: Locator; + + public readonly portInput: Locator; + public readonly rowsPerPageSelect: Locator; + public readonly saveButton: Locator; + + public readonly schemaInput: Locator; + public readonly searchInput: Locator; + public readonly successAlert: Locator; + // Sorting and filtering + public readonly tableHeader: Locator; + public readonly testConnectionButton: Locator; + + public constructor(page: Page) { + super(page); + // Table elements (Chakra UI DataTable) + this.connectionsTable = page.locator('[role="grid"], table'); + this.emptyState = page.locator("text=/No connection found!/i"); + + // Action buttons + this.addButton = page.getByRole("button", { name: "Add Connection" }); + this.testConnectionButton = page.locator('button:has-text("Test")'); + this.saveButton = page.getByRole("button", { name: /^save$/i }); + + // Form inputs (Chakra UI inputs) + this.connectionForm = page.locator('[data-scope="dialog"][data-part="content"]'); + this.connectionIdInput = page.locator('input[name="connection_id"]').first(); + this.connectionTypeSelect = page.getByRole("combobox").first(); + this.hostInput = page.locator('input[name="host"]').first(); + this.portInput = page.locator('input[name="port"]').first(); + this.loginInput = page.locator('input[name="login"]').first(); + this.passwordInput = page.locator('input[name="password"], input[type="password"]').first(); + this.schemaInput = page.locator('input[name="schema"]').first(); + // Try multiple possible selectors + this.descriptionInput = page.locator('[name="description"]').first(); + + // Alerts + this.successAlert = page.locator('[data-scope="toast"][data-part="root"]'); + + // Delete confirmation dialog + this.confirmDeleteButton = page.locator('button:has-text("Delete")').first(); + this.rowsPerPageSelect = page.locator("select"); + + // Sorting and filtering + this.tableHeader = page.locator('[role="columnheader"]').first(); + this.connectionIdHeader = page.locator("th:has-text('Connection ID')").first(); + this.connectionTypeHeader = page.locator('th:has-text("Connection Type")').first(); + this.hostHeader = page.locator('th:has-text("Host")').first(); + this.searchInput = page.locator('input[placeholder*="Search"], input[placeholder*="search"]').first(); + } + + // Click the Add button to create a new connection + public async clickAddButton(): Promise { + await expect(this.addButton).toBeVisible({ timeout: 5000 }); + await expect(this.addButton).toBeEnabled({ timeout: 5000 }); + await this.addButton.click(); + // Wait for form to load + await expect(this.connectionForm).toBeVisible({ timeout: 10_000 }); + } + + // Click edit button for a specific connection + public async clickEditButton(connectionId: string): Promise { + const row = await this.findConnectionRow(connectionId); + + if (!row) { + throw new Error(`Connection ${connectionId} not found`); + } + + const editButton = row.getByRole("button", { name: "Edit Connection" }); + + await expect(editButton).toBeVisible({ timeout: 5000 }); + await expect(editButton).toBeEnabled({ timeout: 5000 }); + await editButton.click(); + await expect(this.connectionForm).toBeVisible({ timeout: 10_000 }); + } + + // Check if a connection exists in the current view + public async connectionExists(connectionId: string): Promise { + const emptyState = await this.page + .locator("text=No connection found!") + .isVisible({ timeout: 1000 }) + .catch(() => false); + + if (emptyState) { + return false; + } + const row = await this.findConnectionRow(connectionId); + const visible = row !== null; + + return visible; + } + + // Create a new connection with full workflow + public async createConnection(details: ConnectionDetails): Promise { + await this.clickAddButton(); + await this.fillConnectionForm(details); + await this.saveConnection(); + await this.waitForConnectionsListLoad(); + } + + // Delete a connection by connection ID + public async deleteConnection(connectionId: string): Promise { + // await this.navigate(); + const row = await this.findConnectionRow(connectionId); + + if (!row) { + throw new Error(`Connection ${connectionId} not found`); + } + + // Find delete button in the row + await this.page.evaluate(() => { + const backdrops = document.querySelectorAll('[data-scope="dialog"][data-part="backdrop"]'); + + backdrops.forEach((backdrop) => { + const { state } = backdrop.dataset; + + if (state === "closed") { + backdrop.remove(); + } + }); + }); + const deleteButton = row.getByRole("button", { name: "Delete Connection" }); + + await expect(deleteButton).toBeVisible({ timeout: 10_000 }); + await expect(deleteButton).toBeEnabled({ timeout: 5000 }); + await deleteButton.click(); + + await expect(this.confirmDeleteButton).toBeVisible({ timeout: 10_000 }); + await expect(this.confirmDeleteButton).toBeEnabled({ timeout: 5000 }); + await this.confirmDeleteButton.click(); + + await expect(this.emptyState).toBeVisible({ timeout: 5000 }); + } + + // Edit a connection by connection ID + public async editConnection(connectionId: string, updates: Partial): Promise { + const row = await this.findConnectionRow(connectionId); + + if (!row) { + throw new Error(`Connection ${connectionId} not found`); + } + + await this.clickEditButton(connectionId); + + // Wait for form to load + await expect(this.connectionIdInput).toBeVisible({ timeout: 10_000 }); + + // Fill the fields that need updating + await this.fillConnectionForm(updates); + await this.saveConnection(); + } + + // Fill connection form with details + public async fillConnectionForm(details: Partial): Promise { + if (details.connection_id !== undefined && details.connection_id !== "") { + await this.connectionIdInput.fill(details.connection_id); + } + + if (details.conn_type !== undefined && details.conn_type !== "") { + // Click the select field to open the dropdown + const selectCombobox = this.page.getByRole("combobox").first(); + + await expect(selectCombobox).toBeEnabled({ timeout: 25_000 }); + + await selectCombobox.click({ timeout: 3000 }); + + // Wait for options to appear and click the matching option + const option = this.page.getByRole("option", { name: new RegExp(details.conn_type, "i") }).first(); + + await option.click({ timeout: 2000 }).catch(() => { + // If option click fails, try typing in the input + if (details.conn_type !== undefined && details.conn_type !== "") { + void this.page.keyboard.type(details.conn_type); + } + }); + } + + if (details.host !== undefined && details.host !== "") { + await expect(this.hostInput).toBeVisible({ timeout: 10_000 }); + await this.hostInput.fill(details.host); + } + + if (details.port !== undefined && details.port !== "") { + await expect(this.portInput).toBeVisible({ timeout: 10_000 }); + await this.portInput.fill(String(details.port)); + } + + if (details.login !== undefined && details.login !== "") { + await expect(this.loginInput).toBeVisible({ timeout: 10_000 }); + await this.loginInput.fill(details.login); + } + + if (details.password !== undefined && details.password !== "") { + await expect(this.passwordInput).toBeVisible({ timeout: 10_000 }); + await this.passwordInput.fill(details.password); + } + + if (details.description !== undefined && details.description !== "") { + await expect(this.descriptionInput).toBeVisible({ timeout: 10_000 }); + await this.descriptionInput.fill(details.description); + } + + if (details.schema !== undefined && details.schema !== "") { + await expect(this.schemaInput).toBeVisible({ timeout: 10_000 }); + await this.schemaInput.fill(details.schema); + } + + if (details.extra !== undefined && details.extra !== "") { + const extraAccordion = this.page.locator('button:has-text("Extra Fields JSON")').first(); + const accordionVisible = await extraAccordion.isVisible({ timeout: 5000 }).catch(() => false); + + if (accordionVisible) { + await extraAccordion.click(); + const extraEditor = this.page.locator('.cm-content[contenteditable="true"]:visible').first(); + + await extraEditor.waitFor({ state: "visible", timeout: 5000 }); + await extraEditor.clear(); + await extraEditor.fill(details.extra); + await extraEditor.blur(); + } + } + } + + // Get connection count from current page + public async getConnectionCount(): Promise { + const ids = await this.getConnectionIds(); + + return ids.length; + } + + // Get all connection IDs from the current page + public async getConnectionIds(): Promise> { + await expect(this.page.locator("tbody tr").first()).toBeVisible({ timeout: 5000 }); + + let stableRowCount = 0; + + await expect + .poll( + async () => { + const count1 = await this.page.locator("tbody tr").count(); + + await this.page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + const count2 = await this.page.locator("tbody tr").count(); + + if (count1 === count2 && count1 > 0) { + stableRowCount = count1; + + return true; + } + + return false; + }, + { intervals: [100, 200, 500], timeout: 10_000 }, + ) + .toBeTruthy() + .catch(() => { + // If timeout, just use current count + stableRowCount = 0; + }); + + if (stableRowCount === 0) { + return []; + } + + let rows = this.page.locator("tbody tr"); + const connectionIds: Array = []; + + // Process all rows + for (let i = 0; i < stableRowCount; i++) { + try { + const row = rows.nth(i); + const cells = row.locator("td"); + const cellCount = await cells.count(); + + if (cellCount > 1) { + // Connection ID is typically in the second cell (after checkbox) + const idCell = cells.nth(1); + const text = await idCell.textContent({ timeout: 3000 }); + + if (text !== null && text.trim() !== "") { + connectionIds.push(text.trim()); + } + } + } catch { + // Skip rows that can't be read + continue; + } + } + + return connectionIds; + } + + // Navigate to Connections list page + public async navigate(): Promise { + await this.navigateTo(ConnectionsPage.connectionsListUrl); + await this.waitForConnectionsListLoad(); + } + + // Save the connection form + public async saveConnection(): Promise { + await expect(this.saveButton).toBeVisible({ timeout: 10_000 }); + await expect(this.saveButton).toBeEnabled({ timeout: 5000 }); + await this.saveButton.click(); + + // Wait for either redirect OR success message + await Promise.race([ + this.page.waitForURL("**/connections", { timeout: 10_000 }), + this.successAlert.waitFor({ state: "visible", timeout: 10_000 }), + ]); + } + + // Search for connections using the search input + public async searchConnections(searchTerm: string): Promise { + await (searchTerm === "" ? this.searchInput.clear() : this.searchInput.fill(searchTerm)); + + // Wait for search to complete by checking results stability + await expect + .poll( + async () => { + const ids = await this.getConnectionIds(); + + // If we expect no results + const isEmptyVisible = await this.emptyState.isVisible().catch(() => false); + + if (isEmptyVisible) { + return ids.length === 0; + } + + // If we expect results, verify they match the search term + if (ids.length === 0) { + return false; // Still loading + } + + if (searchTerm === "") { + // Get count twice to ensure it's stable + const count1 = ids.length; + + await this.page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + const count2 = await this.getConnectionIds().then((allIds) => allIds.length); + + // Stable when count doesn't change + return count1 === count2 && count1 > 0; + } + + // All visible IDs should contain the search term (case-insensitive) + return ids.every((id) => id.toLowerCase().includes(searchTerm.toLowerCase())); + }, + { message: "Search results did not match search term", timeout: 20_000 }, + ) + .toBeTruthy(); + } + + // Verify connection details are displayed in the list + public async verifyConnectionInList(connectionId: string, expectedType: string): Promise { + const row = await this.findConnectionRow(connectionId); + + if (!row) { + throw new Error(`Connection ${connectionId} not found in list`); + } + + const rowText = await row.textContent(); + + expect(rowText).toContain(connectionId); + expect(rowText).toContain(expectedType); + } + + private async findConnectionRow(connectionId: string): Promise { + // Try search first (faster) + const hasSearch = await this.searchInput.isVisible({ timeout: 500 }).catch(() => false); + + if (hasSearch) { + return await this.findConnectionRowUsingSearch(connectionId); + } + + return null; + } + + private async findConnectionRowUsingSearch(connectionId: string): Promise { + await this.searchConnections(connectionId); + + // Check if table is visible (without throwing) + const isTableVisible = await this.connectionsTable.isVisible({ timeout: 5000 }).catch(() => false); + + if (!isTableVisible) { + return null; + } + + const row = this.page.locator("tbody tr").filter({ hasText: connectionId }).first(); + + const rowExists = await row.isVisible({ timeout: 3000 }).catch(() => false); + + if (!rowExists) { + return null; + } + + return row; + } + + // Wait for connections list to fully load + private async waitForConnectionsListLoad(): Promise { + await expect(this.page).toHaveURL(/\/connections/, { timeout: 3000 }); + await this.page.waitForLoadState("domcontentloaded"); + + const table = this.connectionsTable; + + // Wait for either table or empty state + await expect(table.or(this.emptyState)).toBeVisible({ timeout: 10_000 }); + + // If table exists, wait for rows + if (await table.isVisible().catch(() => false)) { + await this.page + .locator("tbody tr") + .first() + .waitFor({ state: "visible", timeout: 10_000 }) + .catch(() => { + // No rows found + }); + + // Wait for row count to stabilize + await expect + .poll( + async () => { + const count1 = await this.page.locator("tbody tr").count(); + + if (count1 === 0) return true; + + await this.page.evaluate(() => new Promise((r) => setTimeout(r, 300))); + const count2 = await this.page.locator("tbody tr").count(); + + return count1 === count2; + }, + { timeout: 15_000 }, + ) + .toBeTruthy() + .catch(() => { + // Timeout - proceed anyway + }); + } + } +} From a4069f3878c1221d0038165b9c7232a4e50fad06 Mon Sep 17 00:00:00 2001 From: vatsrahul1001 Date: Wed, 25 Feb 2026 17:25:05 +0530 Subject: [PATCH 3/4] fix static checks --- .../ui/tests/e2e/pages/ConfigurationPage.ts | 85 ------------------- .../ui/tests/e2e/specs/Configuration.spec.ts | 46 ---------- 2 files changed, 131 deletions(-) delete mode 100644 airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts delete mode 100644 airflow-core/src/airflow/ui/tests/e2e/specs/Configuration.spec.ts diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts deleted file mode 100644 index 0e393040fd8df..0000000000000 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts +++ /dev/null @@ -1,85 +0,0 @@ -/*! - * 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 type { Locator, Page } from "@playwright/test"; - -import { BasePage } from "./BasePage"; - -export class ConfigurationPage extends BasePage { - public readonly heading: Locator; - public readonly rows: Locator; - public readonly table: Locator; - - public constructor(page: Page) { - super(page); - - this.heading = page.getByRole("heading", { - name: /config/i, - }); - this.table = page.getByTestId("table-list"); - this.rows = this.table.locator("tbody tr").filter({ - has: page.locator("td"), - }); - } - - public async getRowCount(): Promise { - return this.rows.count(); - } - - public async getRowDetails(index: number) { - const row = this.rows.nth(index); - const cells = row.locator("td"); - - const section = await cells.nth(0).textContent(); - const key = await cells.nth(1).textContent(); - const value = await cells.nth(2).textContent(); - - return { - key: (key ?? "").trim(), - section: (section ?? "").trim(), - value: (value ?? "").trim(), - }; - } - - public async navigate(): Promise { - await this.navigateTo("/configs"); - } - - public async waitForLoad(): Promise { - await this.table.waitFor({ state: "visible", timeout: 30_000 }); - await this.waitForTableData(); - } - - private async waitForTableData(): Promise { - await this.page.waitForFunction( - () => { - const table = document.querySelector('[data-testid="table-list"]'); - - if (!table) { - return false; - } - - const cells = table.querySelectorAll("tbody tr td"); - - return cells.length > 0; - }, - undefined, - { timeout: 30_000 }, - ); - } -} diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/Configuration.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/Configuration.spec.ts deleted file mode 100644 index ea75fe21f4c4a..0000000000000 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/Configuration.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*! - * 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 { ConfigurationPage } from "../pages/ConfigurationPage"; - -test.describe("Configuration Page", () => { - let configPage: ConfigurationPage; - - test.beforeEach(async ({ page }) => { - configPage = new ConfigurationPage(page); - await configPage.navigate(); - await configPage.waitForLoad(); - }); - - test("verify configuration displays", async () => { - await expect(configPage.heading).toBeVisible(); - await expect(configPage.table).toBeVisible(); - - const count = await configPage.getRowCount(); - - expect(count).toBeGreaterThan(0); - - const { key, section, value } = await configPage.getRowDetails(0); - - expect(section.length).toBeGreaterThan(0); - expect(key.length).toBeGreaterThan(0); - expect(value.length).toBeGreaterThan(0); - }); -}); From 1c18e94c3534f622548ca0623c332dd61088fd64 Mon Sep 17 00:00:00 2001 From: vatsrahul1001 Date: Wed, 25 Feb 2026 18:26:59 +0530 Subject: [PATCH 4/4] fix static checks --- .../src/airflow/ui/tests/e2e/pages/TaskInstancesPage.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/TaskInstancesPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/TaskInstancesPage.ts index 87506271fad50..4fde48e7f4be1 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/TaskInstancesPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/TaskInstancesPage.ts @@ -142,13 +142,4 @@ export class TaskInstancesPage extends BasePage { expect(await rows.count()).toBeGreaterThan(0); } - - /** - * Wait for task instance list to be rendered - */ - private async waitForTaskInstanceList(): Promise { - const dataLink = this.taskInstancesTable.locator("a[href*='/dags/']").first(); - - await expect(dataLink).toBeVisible({ timeout: 10_000 }); - } }