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 75c34ac1cc7 Add E2E tests for Pools page functionality #60567 (#62149)
75c34ac1cc7 is described below
commit 75c34ac1cc7fddf3dba61365cdc007b463baec62
Author: Haseeb Malik <[email protected]>
AuthorDate: Sat Feb 21 04:40:49 2026 -0500
Add E2E tests for Pools page functionality #60567 (#62149)
---
.../src/airflow/ui/tests/e2e/pages/PoolsPage.ts | 178 +++++++++++++++++++++
.../src/airflow/ui/tests/e2e/specs/pools.spec.ts | 99 ++++++++++++
2 files changed, 277 insertions(+)
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/PoolsPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/PoolsPage.ts
new file mode 100644
index 00000000000..805a0bc0ebf
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/PoolsPage.ts
@@ -0,0 +1,178 @@
+/*!
+ * 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";
+
+export class PoolsPage extends BasePage {
+ public static get poolsUrl(): string {
+ return "/pools";
+ }
+
+ public readonly addPoolButton: Locator;
+ public readonly cardList: Locator;
+ public readonly searchInput: Locator;
+
+ public constructor(page: Page) {
+ super(page);
+ this.addPoolButton = page.getByRole("button", { name: "Add Pool" });
+ this.cardList = page.locator('[data-testid="card-list"]');
+ this.searchInput = page.getByPlaceholder("Search Pools");
+ }
+
+ public async createPool(name: string, slots: number, description?: string):
Promise<void> {
+ await this.addPoolButton.click();
+
+ const dialog = this.page.getByRole("dialog");
+
+ await expect(dialog).toBeVisible({ timeout: 10_000 });
+
+ const nameInput = dialog.locator('input[name="name"]');
+
+ await nameInput.fill(name);
+
+ const slotsInput = dialog.locator('input[type="number"]');
+
+ await slotsInput.fill("");
+ await slotsInput.fill(String(slots));
+
+ if (description !== undefined && description !== "") {
+ const descriptionInput = dialog.locator("textarea");
+
+ await descriptionInput.fill(description);
+ }
+
+ const saveButton = dialog.getByRole("button", { name: "Save" });
+
+ await expect(saveButton).toBeEnabled({ timeout: 5000 });
+
+ const responsePromise = this.page.waitForResponse(
+ (response) => response.url().includes("/api/v2/pools") &&
response.request().method() === "POST",
+ { timeout: 10_000 },
+ );
+
+ await saveButton.click();
+ await responsePromise;
+ await this.page.waitForLoadState("networkidle");
+ }
+
+ public async deletePool(poolName: string): Promise<void> {
+ const poolCard = this.getPoolCard(poolName);
+ const deleteButton = poolCard.getByRole("button", { name: "Delete Pool" });
+
+ await deleteButton.click();
+
+ const confirmDialog = this.page.getByRole("dialog");
+
+ await expect(confirmDialog).toBeVisible({ timeout: 10_000 });
+
+ const confirmDeleteButton = confirmDialog.getByRole("button", { name:
"Delete" });
+
+ const responsePromise = this.page.waitForResponse(
+ (response) => response.url().includes("/api/v2/pools") &&
response.request().method() === "DELETE",
+ { timeout: 10_000 },
+ );
+
+ await confirmDeleteButton.click();
+ await responsePromise;
+ await this.page.waitForLoadState("networkidle");
+ }
+
+ public async editPoolSlots(poolName: string, newSlots: number):
Promise<void> {
+ const poolCard = this.getPoolCard(poolName);
+ const editButton = poolCard.getByRole("button", { name: "Edit Pool" });
+
+ await editButton.click();
+
+ const dialog = this.page.getByRole("dialog");
+
+ await expect(dialog).toBeVisible({ timeout: 10_000 });
+
+ const slotsInput = dialog.locator('input[type="number"]');
+
+ await slotsInput.fill("");
+ await slotsInput.fill(String(newSlots));
+
+ const saveButton = dialog.getByRole("button", { name: "Save" });
+
+ await expect(saveButton).toBeEnabled({ timeout: 5000 });
+
+ const responsePromise = this.page.waitForResponse(
+ (response) => response.url().includes("/api/v2/pools") &&
response.request().method() === "PATCH",
+ { timeout: 10_000 },
+ );
+
+ await saveButton.click();
+ await responsePromise;
+ await this.page.waitForLoadState("networkidle");
+ }
+
+ public getPoolCard(poolName: string): Locator {
+ return this.cardList.locator("div").filter({ hasText: poolName }).first();
+ }
+
+ public async navigate(): Promise<void> {
+ await this.navigateTo(PoolsPage.poolsUrl);
+ await this.page.waitForURL("**/pools", { timeout: 15_000 });
+ await this.page.waitForLoadState("networkidle");
+ }
+
+ public async verifyPoolExists(poolName: string): Promise<void> {
+ await this.page.waitForLoadState("networkidle");
+
+ const poolText = this.cardList.getByText(poolName, { exact: false });
+
+ await expect(poolText.first()).toBeVisible({ timeout: 10_000 });
+ }
+
+ public async verifyPoolNotExists(poolName: string): Promise<void> {
+ await this.page.waitForLoadState("networkidle");
+ await this.page.waitForTimeout(1000);
+
+ const poolText = this.cardList.getByText(poolName, { exact: true });
+
+ await expect(poolText).toBeHidden({ timeout: 10_000 });
+ }
+
+ public async verifyPoolsListDisplays(): Promise<void> {
+ await expect(this.addPoolButton).toBeVisible({ timeout: 10_000 });
+ await expect(this.cardList).toBeVisible({ timeout: 10_000 });
+
+ const defaultPoolCard = this.cardList.getByText("default_pool", { exact:
false });
+
+ await expect(defaultPoolCard.first()).toBeVisible({ timeout: 10_000 });
+ }
+
+ public async verifyPoolSlots(poolName: string, expectedSlots: number):
Promise<void> {
+ await this.page.waitForLoadState("networkidle");
+
+ const slotsText = this.cardList.getByText(`${poolName} (${expectedSlots}
Slots)`, { exact: false });
+
+ await expect(slotsText.first()).toBeVisible({ timeout: 10_000 });
+ }
+
+ public async verifyPoolUsageDisplays(poolName: string): Promise<void> {
+ const poolCard = this.getPoolCard(poolName);
+
+ await expect(poolCard).toBeVisible({ timeout: 10_000 });
+
+ const slotsText = poolCard.getByText("Slots", { exact: false });
+
+ await expect(slotsText.first()).toBeVisible({ timeout: 10_000 });
+ }
+}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/pools.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/pools.spec.ts
new file mode 100644
index 00000000000..93574487557
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/pools.spec.ts
@@ -0,0 +1,99 @@
+/*!
+ * 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 } from "@playwright/test";
+import { AUTH_FILE } from "playwright.config";
+import { PoolsPage } from "tests/e2e/pages/PoolsPage";
+
+test.describe("Pools Page", () => {
+ test.setTimeout(60_000);
+
+ let poolsPage: PoolsPage;
+ const testPoolName = `test_pool_${Date.now()}`;
+ const testPoolSlots = 10;
+ const testPoolDescription = "E2E test pool";
+ const updatedSlots = 25;
+ const createDeletePoolName = `test_crud_pool_${Date.now()}`;
+ const createDeletePoolSlots = 5;
+
+ test.beforeAll(async ({ browser }) => {
+ const context = await browser.newContext({ storageState: AUTH_FILE });
+ const page = await context.newPage();
+ const setupPoolsPage = new PoolsPage(page);
+
+ await setupPoolsPage.navigate();
+ await setupPoolsPage.createPool(testPoolName, testPoolSlots,
testPoolDescription);
+
+ await context.close();
+ });
+
+ test.afterAll(async ({ browser }) => {
+ const context = await browser.newContext({ storageState: AUTH_FILE });
+ const page = await context.newPage();
+
+ await
page.request.delete(`/api/v2/pools/${encodeURIComponent(testPoolName)}`);
+ await
page.request.delete(`/api/v2/pools/${encodeURIComponent(createDeletePoolName)}`);
+
+ await context.close();
+ });
+
+ test.beforeEach(({ page }) => {
+ poolsPage = new PoolsPage(page);
+ });
+
+ test("verify pools list displays with default_pool", async () => {
+ await poolsPage.navigate();
+ await poolsPage.verifyPoolsListDisplays();
+ });
+
+ test("verify test pool exists in list", async () => {
+ await poolsPage.navigate();
+ await poolsPage.verifyPoolExists(testPoolName);
+ });
+
+ test("verify pool slots display correctly", async () => {
+ await poolsPage.navigate();
+ await poolsPage.verifyPoolSlots(testPoolName, testPoolSlots);
+ });
+
+ test("edit pool slots", async () => {
+ await poolsPage.navigate();
+ await poolsPage.editPoolSlots(testPoolName, updatedSlots);
+ await poolsPage.navigate();
+ await poolsPage.verifyPoolSlots(testPoolName, updatedSlots);
+ });
+
+ test("verify pool usage displays", async () => {
+ await poolsPage.navigate();
+ await poolsPage.verifyPoolUsageDisplays("default_pool");
+ });
+
+ test("create a new pool", async () => {
+ await poolsPage.navigate();
+ await poolsPage.createPool(createDeletePoolName, createDeletePoolSlots,
"Created pool");
+ await poolsPage.navigate();
+ await poolsPage.verifyPoolExists(createDeletePoolName);
+ });
+
+ test("delete pool", async () => {
+ await poolsPage.navigate();
+ await poolsPage.verifyPoolExists(createDeletePoolName);
+ await poolsPage.deletePool(createDeletePoolName);
+ await poolsPage.verifyPoolNotExists(createDeletePoolName);
+ });
+});