This is an automated email from the ASF dual-hosted git repository.

rahulvats pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new c6a05b5feaa UI E2E tests for Providers Page (#60705)
c6a05b5feaa is described below

commit c6a05b5feaa6834e6987a7c5046f4d2c5b622b95
Author: Sarthak Vaish <[email protected]>
AuthorDate: Thu Feb 5 15:07:35 2026 +0530

    UI E2E tests for Providers Page (#60705)
    
    * E2E tests for Providers Page
---
 .../airflow/ui/tests/e2e/pages/ProvidersPage.ts    | 117 ++++++++++++++++++++
 .../airflow/ui/tests/e2e/specs/providers.spec.ts   | 121 +++++++++++++++++++++
 2 files changed, 238 insertions(+)

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 00000000000..6b597938f33
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts
@@ -0,0 +1,117 @@
+/*!
+ * 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 "./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;
+
+  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"),
+    });
+    this.paginationNextButton = page.locator('[data-testid="next"]');
+    this.paginationPrevButton = page.locator('[data-testid="prev"]');
+  }
+
+  /**
+   * Click next page button
+   */
+  public async clickNextPage(): Promise<void> {
+    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<void> {
+    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<number> {
+    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<void> {
+    await this.navigateTo("/providers");
+  }
+
+  public async providerNames(): Promise<Array<string>> {
+    return this.rows.locator("td a").allTextContents();
+  }
+
+  public async waitForLoad(): Promise<void> {
+    await this.table.waitFor({ state: "visible", timeout: 30_000 });
+    await this.waitForTableData();
+  }
+
+  private async waitForTableData(): Promise<void> {
+    // 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 00000000000..dc20c518d3e
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts
@@ -0,0 +1,121 @@
+/*!
+ * 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 providers 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 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);
+  });
+});

Reply via email to