From 377af1165c522f071aec040ce38db0e54cbb3b08 Mon Sep 17 00:00:00 2001 From: Sarthak Date: Fri, 16 Jan 2026 19:32:57 +0530 Subject: [PATCH 1/2] E2E tests for Providers Page --- .../ui/tests/e2e/pages/ProvidersPage.ts | 89 ++++++++++++++++++ .../ui/tests/e2e/specs/providers.spec.ts | 90 +++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts create mode 100644 airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts new file mode 100644 index 0000000000000..3450a59aa0c29 --- /dev/null +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts @@ -0,0 +1,89 @@ +/*! + * 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 { Page, Locator } from "@playwright/test"; + +import { BasePage } from "./BasePage"; + +export class ProvidersPage 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: /^providers$/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 pkg = await cells.nth(0).locator("a").textContent(); + const ver = await cells.nth(1).textContent(); + const desc = await cells.nth(2).textContent(); + + return { + description: (desc ?? "").trim(), + packageName: (pkg ?? "").trim(), + version: (ver ?? "").trim(), + }; + } + + public async navigate(): Promise { + 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(); + } + + private async waitForTableData(): Promise { + // Wait for actual data links to appear (not skeleton loaders) + await this.page.waitForFunction( + () => { + const table = document.querySelector('[data-testid="table-list"]'); + + if (!table) { + return false; + } + + // Check for actual links in tbody (real data, not skeleton) + const links = table.querySelectorAll("tbody tr td a"); + + return links.length > 0; + }, + undefined, + { timeout: 30_000 }, + ); + } +} 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 new file mode 100644 index 0000000000000..36dee95e7ca9c --- /dev/null +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts @@ -0,0 +1,90 @@ +/*! + * 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 { test, expect } from "@playwright/test"; + +import { ProvidersPage } from "../pages/ProvidersPage"; + +test.describe("Providers Page", () => { + let providers: ProvidersPage; + + test.beforeEach(async ({ page }) => { + providers = new ProvidersPage(page); + await providers.navigate(); + await providers.waitForLoad(); + }); + + test("verify assets page heading", async () => { + await expect(providers.heading).toBeVisible(); + }); + + test("Verify Providers page is accessible via Admin menu", async ({ page }) => { + await page.goto("/"); + + await page.getByRole("button", { name: /^admin$/i }).click(); + + // Click Providers + const providersItem = page.getByRole("menuitem", { name: /^providers$/i }); + + await expect(providersItem).toBeVisible(); + await providersItem.click(); + + await providers.waitForLoad(); + // Assert Providers page loaded + await expect(providers.heading).toBeVisible(); + expect(await providers.getRowCount()).toBeGreaterThan(0); + }); + + test("Verify the providers list displays", async () => { + await expect(providers.table).toBeVisible(); + }); + + test("Verify package name, version, and description are not blank", async () => { + const count = await providers.getRowCount(); + + expect(count).toBeGreaterThan(0); + + for (let i = 0; i < 2; i++) { + const { description, packageName, version } = await providers.getRowDetails(i); + + expect(packageName).not.toEqual(""); + expect(version).not.toEqual(""); + expect(description).not.toEqual(""); + } + }); + + test("verify pagination controls navigate between pages", async () => { + await providers.navigateTo("/providers?limit=5&offset=0"); + await providers.waitForLoad(); + + const page1Initial = await providers.providerNames(); + + expect(page1Initial.length).toBeGreaterThan(0); + + const pagination = providers.page.locator('[data-scope="pagination"]'); + + await pagination.getByRole("button", { name: /^page 2$/i }).click(); + await expect.poll(() => providers.providerNames(), { timeout: 30_000 }).not.toEqual(page1Initial); + + const page2Assets = await providers.providerNames(); + + await pagination.getByRole("button", { name: /page 1/i }).click(); + + await expect.poll(() => providers.providerNames(), { timeout: 30_000 }).not.toEqual(page2Assets); + }); +}); From 1a4394e349b23c11a5dab120a896a6f591d3e2d4 Mon Sep 17 00:00:00 2001 From: Sarthak Date: Mon, 26 Jan 2026 20:27:41 +0530 Subject: [PATCH 2/2] Modified pagination --- .../ui/tests/e2e/pages/ProvidersPage.ts | 30 ++++++++++- .../ui/tests/e2e/specs/providers.spec.ts | 53 +++++++++++++++---- 2 files changed, 71 insertions(+), 12 deletions(-) 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 3450a59aa0c29..6b597938f3358 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import type { Page, Locator } from "@playwright/test"; +import { expect, type Locator, type 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; @@ -33,6 +35,32 @@ 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 { 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 36dee95e7ca9c..dc20c518d3ec5 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 @@ -29,7 +29,7 @@ test.describe("Providers Page", () => { await providers.waitForLoad(); }); - test("verify assets page heading", async () => { + test("verify providers page heading", async () => { await expect(providers.heading).toBeVisible(); }); @@ -68,23 +68,54 @@ test.describe("Providers Page", () => { } }); - test("verify pagination controls navigate between pages", async () => { - await providers.navigateTo("/providers?limit=5&offset=0"); + test("verify providers pagination", async () => { + const limit = 5; + + await providers.navigateTo(`/providers?offset=0&limit=${limit}`); await providers.waitForLoad(); - const page1Initial = await providers.providerNames(); + 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(); - expect(page1Initial.length).toBeGreaterThan(0); + await providers.page.waitForURL((url) => { + const u = new URL(url); + const offset = u.searchParams.get("offset"); - const pagination = providers.page.locator('[data-scope="pagination"]'); + 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 pagination.getByRole("button", { name: /^page 2$/i }).click(); - await expect.poll(() => providers.providerNames(), { timeout: 30_000 }).not.toEqual(page1Initial); + await providers.page.waitForURL((url) => { + const u = new URL(url); + const offset = u.searchParams.get("offset"); - const page2Assets = await providers.providerNames(); + return offset === "0" || offset === null; + }); - await pagination.getByRole("button", { name: /page 1/i }).click(); + const rowsBack = await providers.getRowCount(); - await expect.poll(() => providers.providerNames(), { timeout: 30_000 }).not.toEqual(page2Assets); + expect(rowsBack).toBeGreaterThan(0); }); });