This is an automated email from the ASF dual-hosted git repository.
choo121600 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 f90f1661795 Fix flaky E2E tests with API-based setup and data
isolation (#64024)
f90f1661795 is described below
commit f90f16617957fc239fb9438fcb911e1ca03d073c
Author: Yeonguk Choo <[email protected]>
AuthorDate: Mon May 4 12:58:26 2026 +0900
Fix flaky E2E tests with API-based setup and data isolation (#64024)
---
airflow-core/src/airflow/ui/playwright.config.ts | 11 -
.../airflow/ui/tests/e2e/pages/AssetDetailPage.ts | 8 +-
.../airflow/ui/tests/e2e/pages/AssetListPage.ts | 11 +-
.../src/airflow/ui/tests/e2e/pages/BackfillPage.ts | 82 +-----
.../ui/tests/e2e/pages/ConfigurationPage.ts | 5 +-
.../airflow/ui/tests/e2e/pages/ConnectionsPage.ts | 23 +-
.../src/airflow/ui/tests/e2e/pages/PluginsPage.ts | 1 +
.../airflow/ui/tests/e2e/pages/ProvidersPage.ts | 7 +-
.../ui/tests/e2e/pages/RequiredActionsPage.ts | 16 +-
.../src/airflow/ui/tests/e2e/pages/VariablePage.ts | 1 +
.../src/airflow/ui/tests/e2e/specs/asset.spec.ts | 1 -
.../airflow/ui/tests/e2e/specs/backfill.spec.ts | 73 +++--
.../ui/tests/e2e/specs/configuration.spec.ts | 1 -
.../airflow/ui/tests/e2e/specs/connections.spec.ts | 309 ++++++++++-----------
.../src/airflow/ui/tests/e2e/specs/plugins.spec.ts | 1 -
.../src/airflow/ui/tests/e2e/specs/pools.spec.ts | 90 +++---
.../airflow/ui/tests/e2e/specs/providers.spec.ts | 1 -
.../ui/tests/e2e/specs/requiredAction.spec.ts | 51 ++--
.../airflow/ui/tests/e2e/specs/variable.spec.ts | 174 ++++--------
.../src/airflow/ui/tests/e2e/utils/test-helpers.ts | 8 +-
20 files changed, 354 insertions(+), 520 deletions(-)
diff --git a/airflow-core/src/airflow/ui/playwright.config.ts
b/airflow-core/src/airflow/ui/playwright.config.ts
index a7632fdf54f..301e59c01dc 100644
--- a/airflow-core/src/airflow/ui/playwright.config.ts
+++ b/airflow-core/src/airflow/ui/playwright.config.ts
@@ -110,17 +110,6 @@ export default defineConfig({
retries: process.env.CI !== undefined && process.env.CI !== "" ? 4 : 0,
testDir: "./tests/e2e/specs",
- // TODO: Temporarily ignore flaky specs until stabilized
- // See: #63036
- testIgnore: [
- "**/dag-runs-tab.spec.ts",
- "**/dag-runs.spec.ts",
- "**/dag-grid-view.spec.ts",
- "**/task-logs.spec.ts",
- "**/dag-tasks.spec.ts",
- "**/variable.spec.ts",
- "**/xcoms.spec.ts",
- ],
timeout: 60_000,
use: {
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/AssetDetailPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/AssetDetailPage.ts
index 33402a9dcde..432d700cafc 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/AssetDetailPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/AssetDetailPage.ts
@@ -60,13 +60,9 @@ export class AssetDetailPage extends BasePage {
* Uses stable selectors based on text content and ARIA roles
*/
private async verifyStatSection(labelText: string): Promise<void> {
- const label = this.page.getByText(labelText, { exact: true });
+ const statContainer = this.page.getByTestId("stat").filter({ hasText:
labelText });
- await expect(label).toBeVisible();
-
- // Find the button that follows the label in the same stat section
- // Navigate to the label's parent container and find the first button
within it
- const statContainer = label.locator("xpath=./parent::*");
+ await expect(statContainer).toBeVisible();
const button = statContainer.getByRole("button").first();
await expect(button).toBeVisible();
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/AssetListPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/AssetListPage.ts
index 19300a33024..58e0c75a7c3 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/AssetListPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/AssetListPage.ts
@@ -46,6 +46,7 @@ export class AssetListPage extends BasePage {
await expect(async () => {
await this.navigateTo("/assets");
await this.page.waitForURL(/.*assets/, { timeout: 10_000 });
+ await this.waitForLoad();
}).toPass({ intervals: [2000], timeout: 60_000 });
}
@@ -66,15 +67,11 @@ export class AssetListPage extends BasePage {
public async search(value: string): Promise<void> {
await this.searchInput.fill(value);
- await this.waitForTableData();
+ await this.waitForLoad();
}
public async waitForLoad(): Promise<void> {
- await this.table.waitFor({ state: "visible", timeout: 30_000 });
- await this.waitForTableData();
- }
-
- private async waitForTableData(): Promise<void> {
- await expect(this.rows.locator("td a").first()).toBeVisible({ timeout:
30_000 });
+ await expect(this.table).toBeVisible({ timeout: 10_000 });
+ await expect(this.rows.locator("td a").first()).toBeVisible({ timeout:
10_000 });
}
}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts
index b73d78c24f5..425c5cb543e 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts
@@ -20,6 +20,12 @@ import { expect } from "@playwright/test";
import type { Locator, Page } from "@playwright/test";
import { testConfig } from "playwright.config";
import { BasePage } from "tests/e2e/pages/BasePage";
+import {
+ apiCancelAllActiveBackfills,
+ apiCancelBackfill,
+ apiWaitForBackfillComplete,
+ apiWaitForNoActiveBackfill,
+} from "tests/e2e/utils/test-helpers";
export const REPROCESS_API_TO_UI = {
completed: "All Runs",
@@ -54,10 +60,6 @@ type BackfillApiResponse = {
id: number;
};
-type BackfillListApiResponse = {
- backfills: Array<BackfillApiResponse>;
-};
-
const {
connection: { baseUrl },
} = testConfig;
@@ -114,26 +116,11 @@ export class BackfillPage extends BasePage {
}
public async cancelAllActiveBackfillsViaApi(dagId: string): Promise<void> {
- const response = await
this.page.request.get(`${baseUrl}/api/v2/backfills?dag_id=${dagId}&limit=100`, {
- timeout: 30_000,
- });
-
- expect(response.ok()).toBe(true);
- const data = (await response.json()) as BackfillListApiResponse;
-
- for (const backfill of data.backfills) {
- if (backfill.completed_at === null) {
- await this.cancelBackfillViaApi(backfill.id);
- }
- }
+ await apiCancelAllActiveBackfills(this.page, dagId);
}
public async cancelBackfillViaApi(backfillId: number): Promise<void> {
- const response = await
this.page.request.put(`${baseUrl}/api/v2/backfills/${backfillId}/cancel`, {
- timeout: 30_000,
- });
-
- expect([200, 409]).toContain(response.status());
+ await apiCancelBackfill(this.page, backfillId);
}
public async clickCancelButton(): Promise<void> {
@@ -464,59 +451,10 @@ export class BackfillPage extends BasePage {
}
public async waitForBackfillComplete(backfillId: number, timeout: number =
120_000): Promise<void> {
- await expect
- .poll(
- async () => {
- try {
- const response = await
this.page.request.get(`${baseUrl}/api/v2/backfills/${backfillId}`, {
- timeout: 30_000,
- });
-
- if (!response.ok()) {
- return false;
- }
- const data = (await response.json()) as BackfillApiResponse;
-
- return data.completed_at !== null;
- } catch {
- return false;
- }
- },
- {
- intervals: [2000, 5000, 10_000],
- message: `Backfill ${backfillId} did not complete within
${timeout}ms`,
- timeout,
- },
- )
- .toBeTruthy();
+ await apiWaitForBackfillComplete(this.page, backfillId, timeout);
}
public async waitForNoActiveBackfillViaApi(dagId: string, timeout: number =
120_000): Promise<void> {
- await expect
- .poll(
- async () => {
- try {
- const response = await this.page.request.get(
- `${baseUrl}/api/v2/backfills?dag_id=${dagId}&limit=100`,
- { timeout: 30_000 },
- );
-
- if (!response.ok()) {
- return false;
- }
- const data = (await response.json()) as BackfillListApiResponse;
-
- return data.backfills.every((b) => b.completed_at !== null);
- } catch {
- return false;
- }
- },
- {
- intervals: [2000, 5000, 10_000],
- message: `Active backfills for Dag ${dagId} did not clear within
${timeout}ms`,
- timeout,
- },
- )
- .toBeTruthy();
+ await apiWaitForNoActiveBackfill(this.page, dagId, timeout);
}
}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts
index d6b74c030b5..7dd9cc27b68 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/ConfigurationPage.ts
@@ -42,11 +42,12 @@ export class ConfigurationPage extends BasePage {
await expect(async () => {
await this.navigateTo("/configs");
await this.page.waitForURL(/.*configs/, { timeout: 10_000 });
+ await this.waitForLoad();
}).toPass({ intervals: [2000], timeout: 60_000 });
}
public async waitForLoad(): Promise<void> {
- await this.table.waitFor({ state: "visible", timeout: 30_000 });
- await this.rows.first().waitFor({ state: "visible", timeout: 30_000 });
+ await expect(this.table).toBeVisible({ timeout: 10_000 });
+ await expect(this.rows.first()).toBeVisible({ timeout: 10_000 });
}
}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts
index 20fbecb6d5a..77eef9f28b7 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts
@@ -270,22 +270,23 @@ export class ConnectionsPage extends BasePage {
return [];
}
+ const headerTexts = await this.page.locator("thead th").allTextContents();
+ const idColumnIndex = headerTexts.findIndex((text) =>
/connection\s*id/i.test(text));
+
+ if (idColumnIndex === -1) {
+ throw new Error(`"Connection ID" column not found in headers:
${JSON.stringify(headerTexts)}`);
+ }
+
+ const rows = this.page.locator("tbody tr");
const connectionIds: Array<string> = [];
for (let i = 0; i < stableRowCount; i++) {
try {
- const row = rowLocator.nth(i);
- const cells = row.locator("td");
- const cellCount = await cells.count();
+ const cell = rows.nth(i).locator("td").nth(idColumnIndex);
+ const text = await cell.textContent({ timeout: 3000 });
- if (cellCount > 1) {
- // Second cell (after checkbox) contains the connection ID.
- const idCell = cells.nth(1);
- const text = await idCell.textContent({ timeout: 3000 });
-
- if (text !== null && text.trim() !== "") {
- connectionIds.push(text.trim());
- }
+ if (text !== null && text.trim() !== "") {
+ connectionIds.push(text.trim());
}
} catch {
continue;
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/PluginsPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/PluginsPage.ts
index 636048b8949..806e07c88ac 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/PluginsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/PluginsPage.ts
@@ -42,6 +42,7 @@ export class PluginsPage extends BasePage {
await expect(async () => {
await this.navigateTo("/plugins");
await this.page.waitForURL(/.*plugins/, { timeout: 10_000 });
+ await this.waitForLoad();
}).toPass({ intervals: [2000], timeout: 60_000 });
}
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 a894f225f34..156fe8c7e99 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts
@@ -63,7 +63,7 @@ export class ProvidersPage extends BasePage {
public async navigate(): Promise<void> {
await expect(async () => {
await this.navigateTo("/providers");
- await this.page.waitForURL(/.*providers/, { timeout: 10_000 });
+ await this.waitForLoad();
}).toPass({ intervals: [2000], timeout: 60_000 });
}
@@ -85,8 +85,7 @@ export class ProvidersPage extends BasePage {
}
public async waitForLoad(): Promise<void> {
- await expect(this.table).toBeVisible({ timeout: 30_000 });
- await expect(this.rows.first()).toBeVisible({ timeout: 30_000 });
- await expect(this.packageLinkAt(0)).toBeVisible({ timeout: 30_000 });
+ await expect(this.table).toBeVisible({ timeout: 10_000 });
+ await expect(this.rows.first()).toBeVisible({ timeout: 10_000 });
}
}
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 99a77923b4c..d85800cc7ba 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts
@@ -18,9 +18,9 @@
*/
import { expect, type APIRequestContext, type Locator, type Page } from
"@playwright/test";
import { testConfig } from "playwright.config";
+import { apiTriggerDagRun, waitForDagReady } from
"tests/e2e/utils/test-helpers";
import { BasePage } from "./BasePage";
-import { DagsPage } from "./DagsPage";
export class RequiredActionsPage extends BasePage {
public readonly actionsTable: Locator;
@@ -100,7 +100,7 @@ export class RequiredActionsPage extends BasePage {
await expect(taskLocator).toBeVisible({ timeout: 20_000 });
await this.page.mouse.move(0, 0);
await taskLocator.click({ timeout: 5000 });
- }).toPass({ intervals: [5000, 10_000, 15_000], timeout: 120_000 });
+ }).toPass({ intervals: [15_000], timeout: 120_000 });
}
private async clickRequiredActionLink(): Promise<void> {
@@ -228,13 +228,13 @@ export class RequiredActionsPage extends BasePage {
}
private async runHITLFlow(dagId: string, approve: boolean): Promise<string> {
- const dagsPage = new DagsPage(this.page);
-
- const dagRunId = await dagsPage.triggerDag(dagId);
+ const { baseUrl } = testConfig.connection;
- if (dagRunId === null) {
- throw new Error("Failed to trigger Dag - dagRunId is null");
- }
+ await waitForDagReady(this.apiRequest, dagId);
+ await this.apiRequest.patch(`${baseUrl}/api/v2/dags/${dagId}`, {
+ data: { is_paused: false },
+ });
+ const { dagRunId } = await apiTriggerDagRun(this.apiRequest, dagId);
await this.waitForDagRunState(dagId, dagRunId, "Running");
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/VariablePage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/VariablePage.ts
index 6db90a998aa..7fd574ed2ca 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/VariablePage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/VariablePage.ts
@@ -55,6 +55,7 @@ export class VariablePage extends BasePage {
await expect(async () => {
await this.navigateTo("/variables");
await this.page.waitForURL(/.*variables/, { timeout: 10_000 });
+ await this.waitForLoad();
}).toPass({ intervals: [2000], timeout: 60_000 });
}
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 f1f11eed513..f3708586e36 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
@@ -24,7 +24,6 @@ test.describe("Assets Page", () => {
// assetData is triggered once per worker via beforeEach.
test.beforeEach(async ({ assetData: _data, assetListPage }) => {
await assetListPage.navigate();
- await assetListPage.waitForLoad();
});
test("verify assets page heading", async ({ assetListPage }) => {
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts
index f47d40e3171..6168d3afbdd 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts
@@ -16,10 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { test, expect } from "@playwright/test";
-import { testConfig, AUTH_FILE } from "playwright.config";
-import { BackfillPage, REPROCESS_API_TO_UI } from
"tests/e2e/pages/BackfillPage";
+import { testConfig } from "playwright.config";
+import { expect, test } from "tests/e2e/fixtures";
+import { REPROCESS_API_TO_UI } from "tests/e2e/pages/BackfillPage";
import type { ReprocessBehaviorApi } from "tests/e2e/pages/BackfillPage";
+import {
+ apiCancelAllActiveBackfills,
+ apiCreateBackfill,
+ apiWaitForBackfillComplete,
+ waitForDagReady,
+} from "tests/e2e/utils/test-helpers";
// Fixed past dates avoid non-determinism from relative date calculations.
// Controls tests use wide, non-overlapping ranges so the scheduler cannot
@@ -53,44 +59,34 @@ test.describe("Backfill", () => {
{ behavior: "failed", dates: FIXED_DATES.set3 },
];
- test.beforeAll(async ({ browser }) => {
+ test.beforeAll(async ({ authenticatedRequest }) => {
test.setTimeout(300_000);
- const context = await browser.newContext({ storageState: AUTH_FILE });
- const page = await context.newPage();
- const setupPage = new BackfillPage(page);
-
- await setupPage.navigateToDagDetail(testDagId);
- await setupPage.cancelAllActiveBackfillsViaApi(testDagId);
+ await waitForDagReady(authenticatedRequest, testDagId);
+ await authenticatedRequest.patch(`/api/v2/dags/${testDagId}`, {
+ data: { is_paused: false },
+ });
+ await apiCancelAllActiveBackfills(authenticatedRequest, testDagId);
for (const config of backfillConfigs) {
- const backfillId = await setupPage.createBackfill(testDagId, {
+ const backfillId = await apiCreateBackfill(authenticatedRequest,
testDagId, {
fromDate: config.dates.from,
reprocessBehavior: config.behavior,
toDate: config.dates.to,
});
- await setupPage.waitForBackfillComplete(backfillId);
+ await apiWaitForBackfillComplete(authenticatedRequest, backfillId,
240_000);
}
-
- await context.close();
});
- test.afterAll(async ({ browser }) => {
- const context = await browser.newContext({ storageState: AUTH_FILE });
- const page = await context.newPage();
- const cleanupPage = new BackfillPage(page);
-
- await cleanupPage.cancelAllActiveBackfillsViaApi(testDagId);
- await context.close();
+ test.afterAll(async ({ authenticatedRequest }) => {
+ await apiCancelAllActiveBackfills(authenticatedRequest,
testDagId).catch(() => undefined);
});
for (const config of backfillConfigs) {
- test.fixme(`verify backfill with
'${REPROCESS_API_TO_UI[config.behavior]}' behavior`, async ({
- page,
+ test(`verify backfill with '${REPROCESS_API_TO_UI[config.behavior]}'
behavior`, async ({
+ backfillPage,
}) => {
- const backfillPage = new BackfillPage(page);
-
await backfillPage.navigateToBackfillsTab(testDagId);
const details = await backfillPage.getBackfillDetailsByDateRange({
@@ -106,9 +102,7 @@ test.describe("Backfill", () => {
});
}
- test.fixme("Verify backfill table filters", async ({ page }) => {
- const backfillPage = new BackfillPage(page);
-
+ test("Verify backfill table filters", async ({ backfillPage }) => {
await backfillPage.navigateToBackfillsTab(testDagId);
const tableHeaders = backfillPage.backfillsTable.locator("thead th");
@@ -120,7 +114,7 @@ test.describe("Backfill", () => {
await backfillPage.openFilterMenu();
- const filterMenuItems = page.getByRole("menuitem");
+ const filterMenuItems = backfillPage.page.getByRole("menuitem");
await expect(filterMenuItems).not.toHaveCount(0);
@@ -130,7 +124,7 @@ test.describe("Backfill", () => {
expect(columnToToggle).not.toBe("");
await backfillPage.toggleColumn(columnToToggle);
- await page.keyboard.press("Escape");
+ await backfillPage.page.keyboard.press("Escape");
await
expect(backfillPage.getColumnHeader(columnToToggle)).not.toBeVisible();
@@ -138,7 +132,7 @@ test.describe("Backfill", () => {
await backfillPage.openFilterMenu();
await backfillPage.toggleColumn(columnToToggle);
- await page.keyboard.press("Escape");
+ await backfillPage.page.keyboard.press("Escape");
await expect(backfillPage.getColumnHeader(columnToToggle)).toBeVisible();
@@ -151,9 +145,7 @@ test.describe("Backfill", () => {
const testDagId = testConfig.testDag.id;
- test("verify date range selection (start date, end date)", async ({ page
}) => {
- const backfillPage = new BackfillPage(page);
-
+ test("verify date range selection (start date, end date)", async ({
backfillPage }) => {
await backfillPage.navigateToDagDetail(testDagId);
await backfillPage.openBackfillDialog();
await backfillPage.backfillFromDateInput.fill("2025-01-10T00:00");
@@ -168,19 +160,16 @@ test.describe("Backfill", () => {
const testDagId = testConfig.testDag.id;
- let backfillPage: BackfillPage;
-
- test.beforeEach(async ({ page }) => {
- backfillPage = new BackfillPage(page);
+ test.beforeEach(async ({ backfillPage }) => {
await backfillPage.cancelAllActiveBackfillsViaApi(testDagId);
await backfillPage.waitForNoActiveBackfillViaApi(testDagId, 30_000);
});
- test.afterEach(async () => {
+ test.afterEach(async ({ backfillPage }) => {
await backfillPage.cancelAllActiveBackfillsViaApi(testDagId);
});
- test("verify pause and resume backfill", async () => {
+ test("verify pause and resume backfill", async ({ backfillPage }) => {
const dates = FIXED_DATES.controls.resumePause;
// Create + pause atomically to eliminate race with scheduler.
@@ -201,7 +190,7 @@ test.describe("Backfill", () => {
await expect(backfillPage.unpauseButton).toBeVisible({ timeout: 10_000
});
});
- test.fixme("verify cancel backfill", async () => {
+ test("verify cancel backfill", async ({ backfillPage }) => {
const dates = FIXED_DATES.controls.cancel;
// Create + pause atomically to eliminate race with scheduler.
@@ -219,7 +208,7 @@ test.describe("Backfill", () => {
await expect(backfillPage.cancelButton).not.toBeVisible({ timeout:
10_000 });
});
- test("verify cancelled backfill cannot be resumed", async () => {
+ test("verify cancelled backfill cannot be resumed", async ({ backfillPage
}) => {
const dates = FIXED_DATES.controls.cancelledNoResume;
// Setup via API: create and cancel directly (UI cancel is tested above).
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
index b1aa8d1490a..fd35506aac0 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/configuration.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/configuration.spec.ts
@@ -21,7 +21,6 @@ import { expect, test } from "tests/e2e/fixtures";
test.describe("Configuration Page", () => {
test.beforeEach(async ({ configurationPage }) => {
await configurationPage.navigate();
- await configurationPage.waitForLoad();
});
test("verify configuration displays", async ({ configurationPage }) => {
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/connections.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/connections.spec.ts
index eb8ed665ce9..a58765ff8c1 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/connections.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/connections.spec.ts
@@ -16,189 +16,171 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { expect, test } from "@playwright/test";
-import { testConfig, AUTH_FILE } from "playwright.config";
-import { ConnectionsPage } from "tests/e2e/pages/ConnectionsPage";
+import { expect, test } from "tests/e2e/fixtures";
+import { uniqueRunId } from "tests/e2e/utils/test-helpers";
test.describe("Connections Page - List and Display", () => {
- let connectionsPage: ConnectionsPage;
- const { baseUrl } = testConfig.connection;
- const timestamp = Date.now();
- const seedConnection = {
- conn_type: "http",
- connection_id: `list_seed_conn_${timestamp}`,
- host: "seed.example.com",
- };
-
- test.beforeEach(({ page }) => {
- connectionsPage = new ConnectionsPage(page);
- });
+ let seedConnection: { conn_type: string; connection_id: string; host: string
};
- test.beforeAll(async ({ browser }) => {
- const context = await browser.newContext({ storageState: AUTH_FILE });
- const page = await context.newPage();
+ test.beforeAll(async ({ authenticatedRequest }) => {
+ seedConnection = {
+ conn_type: "http",
+ connection_id: `list_seed_conn_${uniqueRunId("t")}`,
+ host: "seed.example.com",
+ };
- const response = await page.request.post(`${baseUrl}/api/v2/connections`, {
+ const response = await authenticatedRequest.post(`/api/v2/connections`, {
data: seedConnection,
headers: { "Content-Type": "application/json" },
+ timeout: 30_000,
});
expect([200, 201, 409]).toContain(response.status());
- await context.close();
});
- test.afterAll(async ({ browser }) => {
- const context = await browser.newContext({ storageState: AUTH_FILE });
- const page = await context.newPage();
-
- const response = await
page.request.delete(`${baseUrl}/api/v2/connections/list_seed_conn_${timestamp}`);
+ test.afterAll(async ({ authenticatedRequest }) => {
+ const response = await authenticatedRequest.delete(
+ `/api/v2/connections/${seedConnection.connection_id}`,
+ { timeout: 30_000 },
+ );
- expect([204, 404]).toContain(response.status());
- await context.close();
+ if (response.status() !== 404 && !response.ok()) {
+ console.warn(`Cleanup failed for ${seedConnection.connection_id}:
${response.status()}`);
+ }
});
- test("should display connections list page", async () => {
+ test("should display connections list page", async ({ connectionsPage }) => {
await connectionsPage.navigate();
- // Verify the page is loaded
expect(connectionsPage.page.url()).toContain("/connections");
- // Verify table or list is visible
await expect(connectionsPage.connectionsTable).toBeVisible();
});
- test("should display connections with correct columns", async () => {
+ test("should display connections with correct columns", async ({
connectionsPage }) => {
await connectionsPage.navigate();
- // Check that we have at least one row
- const count = await connectionsPage.getConnectionCount();
-
- expect(count).toBeGreaterThan(0);
+ await expect
+ .poll(async () => connectionsPage.getConnectionCount(), { intervals:
[1000], timeout: 15_000 })
+ .toBeGreaterThan(0);
- // Verify column headers exist
await expect(connectionsPage.connectionIdHeader).toBeVisible();
await expect(connectionsPage.connectionTypeHeader).toBeVisible();
await expect(connectionsPage.hostHeader).toBeVisible();
- // Verify the seed connection is displayed in the list
await connectionsPage.verifyConnectionInList(seedConnection.connection_id,
seedConnection.conn_type);
});
- test("should have Add button visible", async () => {
+ test("should have Add button visible", async ({ connectionsPage }) => {
await connectionsPage.navigate();
await expect(connectionsPage.addButton).toBeVisible();
});
});
test.describe("Connections Page - CRUD Operations", () => {
- let connectionsPage: ConnectionsPage;
- const { baseUrl } = testConfig.connection;
- const timestamp = Date.now();
-
- // Connection created via API in beforeAll - used for edit and display tests
- const existingConnection = {
- conn_type: "postgres",
- connection_id: `existing_conn_${timestamp}`,
- host: `existing-host-${timestamp}.example.com`,
- login: `existing_user_${timestamp}`,
+ let existingConnection: { conn_type: string; connection_id: string; host:
string; login: string };
+ let updatedConnection: {
+ conn_type: string;
+ description: string;
+ host: string;
+ login: string;
+ port: number;
};
- const updatedConnection = {
- conn_type: "postgres",
- description: `Updated test connection at ${new Date().toISOString()}`,
- host: `updated-host-${timestamp}.example.com`,
- login: `updated_user_${timestamp}`,
- port: 5433,
- };
+ const createdConnIds: Array<string> = [];
- // Connection created via UI in test - used for create and delete tests
- const newConnection = {
- conn_type: "postgres",
- connection_id: `new_conn_${timestamp}`,
- description: `Test connection created at ${new Date().toISOString()}`,
- extra: JSON.stringify({
- options: "-c statement_timeout=5000",
- sslmode: "require",
- }),
- host: `new-host-${timestamp}.example.com`,
- login: `new_user_${timestamp}`,
- password: `new_password_${timestamp}`,
- port: 5432,
- schema: "test_db",
- };
+ test.beforeAll(async ({ authenticatedRequest }) => {
+ const timestamp = uniqueRunId("t");
- test.beforeEach(({ page }) => {
- connectionsPage = new ConnectionsPage(page);
- });
+ existingConnection = {
+ conn_type: "postgres",
+ connection_id: `existing_conn_${timestamp}`,
+ host: `existing-host-${timestamp}.example.com`,
+ login: `existing_user_${timestamp}`,
+ };
- test.beforeAll(async ({ browser }) => {
- // Create existing connection via API for edit and display tests
- const context = await browser.newContext({ storageState: AUTH_FILE });
- const page = await context.newPage();
+ updatedConnection = {
+ conn_type: "postgres",
+ description: `Updated test connection at ${new Date().toISOString()}`,
+ host: `updated-host-${timestamp}.example.com`,
+ login: `updated_user_${timestamp}`,
+ port: 5433,
+ };
- await page.request.post(`${baseUrl}/api/v2/connections`, {
+ await authenticatedRequest.post(`/api/v2/connections`, {
data: existingConnection,
headers: { "Content-Type": "application/json" },
+ timeout: 30_000,
});
-
- await context.close();
});
- test.afterAll(async ({ browser }) => {
- // Cleanup all test connections via API
- const context = await browser.newContext({ storageState: AUTH_FILE });
- const page = await context.newPage();
-
- for (const connId of [
- existingConnection.connection_id,
- newConnection.connection_id,
- `temp_conn_${timestamp}_delete`,
- ]) {
- await page.request.delete(`${baseUrl}/api/v2/connections/${connId}`);
- }
+ test.afterAll(async ({ authenticatedRequest }) => {
+ for (const connId of [existingConnection.connection_id,
...createdConnIds]) {
+ const response = await
authenticatedRequest.delete(`/api/v2/connections/${connId}`, {
+ timeout: 30_000,
+ });
- await context.close();
+ if (response.status() !== 404 && !response.ok()) {
+ console.warn(`Cleanup failed for ${connId}: ${response.status()}`);
+ }
+ }
});
- test.fixme("should create a new connection and display it in list", async ()
=> {
- test.setTimeout(120_000);
- await connectionsPage.navigate();
+ test("should create a new connection and display it in list", async ({
connectionsPage }) => {
+ test.slow();
+
+ const connId = `new_conn_${uniqueRunId("create")}`;
+
+ createdConnIds.push(connId);
- // Create connection via UI
+ const newConnection = {
+ conn_type: "postgres",
+ connection_id: connId,
+ description: `Test connection created at ${new Date().toISOString()}`,
+ extra: JSON.stringify({
+ options: "-c statement_timeout=5000",
+ sslmode: "require",
+ }),
+ host: `new-host-${connId}.example.com`,
+ login: `new_user_${connId}`,
+ password: `new_password_${connId}`,
+ port: 5432,
+ schema: "test_db",
+ };
+
+ await connectionsPage.navigate();
await connectionsPage.createConnection(newConnection);
+
const exists = await
connectionsPage.connectionExists(newConnection.connection_id);
expect(exists).toBeTruthy();
- // Verify it appears in the list with correct type
await connectionsPage.verifyConnectionInList(newConnection.connection_id,
newConnection.conn_type);
});
- test.fixme("should edit an existing connection", async () => {
- test.setTimeout(120_000);
+ test("should edit an existing connection", async ({ connectionsPage }) => {
+ test.slow();
await connectionsPage.navigate();
- // Verify connection exists before editing (created in beforeAll)
const exists = await
connectionsPage.connectionExists(existingConnection.connection_id);
expect(exists).toBeTruthy();
- // Edit the connection
await connectionsPage.editConnection(existingConnection.connection_id,
updatedConnection);
- // Verify the connection still exists after editing
const stillExists = await
connectionsPage.connectionExists(existingConnection.connection_id);
expect(stillExists).toBeTruthy();
});
- test("should delete a connection", async () => {
- test.setTimeout(120_000);
+ test("should delete a connection", async ({ connectionsPage }) => {
+ test.slow();
+
+ const tempConnId = `temp_conn_${uniqueRunId("del")}`;
- // Create a temporary connection for deletion test
const tempConnection = {
conn_type: "postgres",
- connection_id: `temp_conn_${timestamp}_delete`,
- host: `temp-host-${timestamp}.example.com`,
+ connection_id: tempConnId,
+ host: "temp-host.example.com",
login: "temp_user",
password: "temp_password",
};
@@ -210,7 +192,6 @@ test.describe("Connections Page - CRUD Operations", () => {
expect(exists).toBeTruthy();
- // Delete the connection
await connectionsPage.deleteConnection(tempConnection.connection_id);
const stillExists = await
connectionsPage.connectionExists(tempConnection.connection_id);
@@ -220,70 +201,68 @@ test.describe("Connections Page - CRUD Operations", () =>
{
});
test.describe("Connections Page - Search and Filter", () => {
- let connectionsPage: ConnectionsPage;
- const { baseUrl } = testConfig.connection;
- const timestamp = Date.now();
-
- const searchTestConnections = [
- {
- conn_type: "postgres",
- connection_id: `production_search_${timestamp}`,
- host: "prod-db.example.com",
- login: "prod_user",
- },
- {
- conn_type: "mysql",
- connection_id: `staging_search_${timestamp}`,
- host: "staging-db.example.com",
- login: "staging_user",
- },
- {
- conn_type: "http",
- connection_id: `development_search_${timestamp}`,
- host: "dev-api.example.com",
- login: "dev_user",
- },
- ];
-
- test.beforeEach(({ page }) => {
- connectionsPage = new ConnectionsPage(page);
- });
-
- test.beforeAll(async ({ browser }) => {
- // Create test connections
- const context = await browser.newContext({ storageState: AUTH_FILE });
- const page = await context.newPage();
+ let searchTestConnections: Array<{
+ conn_type: string;
+ connection_id: string;
+ host: string;
+ login: string;
+ }>;
+
+ test.beforeAll(async ({ authenticatedRequest }) => {
+ const timestamp = uniqueRunId("t");
+
+ searchTestConnections = [
+ {
+ conn_type: "postgres",
+ connection_id: `production_search_${timestamp}`,
+ host: "prod-db.example.com",
+ login: "prod_user",
+ },
+ {
+ conn_type: "mysql",
+ connection_id: `staging_search_${timestamp}`,
+ host: "staging-db.example.com",
+ login: "staging_user",
+ },
+ {
+ conn_type: "http",
+ connection_id: `development_search_${timestamp}`,
+ host: "dev-api.example.com",
+ login: "dev_user",
+ },
+ ];
for (const conn of searchTestConnections) {
- const response = await
page.request.post(`${baseUrl}/api/v2/connections`, {
+ const response = await authenticatedRequest.post(`/api/v2/connections`, {
data: JSON.stringify(conn),
headers: {
"Content-Type": "application/json",
},
+ timeout: 30_000,
});
expect([200, 201, 409]).toContain(response.status());
}
});
- test.afterAll(async ({ browser }) => {
- // Cleanup
- const context = await browser.newContext({ storageState: AUTH_FILE });
- const page = await context.newPage();
-
+ test.afterAll(async ({ authenticatedRequest }) => {
for (const conn of searchTestConnections) {
- const response = await
page.request.delete(`${baseUrl}/api/v2/connections/${conn.connection_id}`);
+ const response = await
authenticatedRequest.delete(`/api/v2/connections/${conn.connection_id}`, {
+ timeout: 30_000,
+ });
- expect([204, 404]).toContain(response.status());
+ if (response.status() !== 404 && !response.ok()) {
+ console.warn(`Cleanup failed for ${conn.connection_id}:
${response.status()}`);
+ }
}
});
- test("should filter connections by search term", async () => {
+ test("should filter connections by search term", async ({ connectionsPage })
=> {
await connectionsPage.navigate();
- const initialCount = await connectionsPage.getConnectionCount();
-
- expect(initialCount).toBeGreaterThan(0);
+ await expect
+ .poll(async () => connectionsPage.getConnectionCount(), { intervals:
[1000], timeout: 30_000 })
+ .toBeGreaterThan(0);
const searchTerm = "production";
@@ -294,7 +273,6 @@ test.describe("Connections Page - Search and Filter", () =>
{
async () => {
const ids = await connectionsPage.getConnectionIds();
- // Verify we have results AND they match the search term
return ids.length > 0 && ids.every((id) =>
id.toLowerCase().includes(searchTerm.toLowerCase()));
},
{ intervals: [500], timeout: 10_000 },
@@ -309,18 +287,24 @@ test.describe("Connections Page - Search and Filter", ()
=> {
}
});
- test("should display all connections when search is cleared", async () => {
- test.setTimeout(120_000);
+ test("should display all connections when search is cleared", async ({
connectionsPage }) => {
+ test.slow();
await connectionsPage.navigate();
- const initialCount = await connectionsPage.getConnectionCount();
+ let initialCount = 0;
- expect(initialCount).toBeGreaterThan(0);
+ await expect
+ .poll(
+ async () => {
+ initialCount = await connectionsPage.getConnectionCount();
- // Search for something
- await connectionsPage.searchConnections("production");
+ return initialCount;
+ },
+ { intervals: [1000], timeout: 30_000 },
+ )
+ .toBeGreaterThan(0);
- // Wait for search results
+ await connectionsPage.searchConnections("production");
await expect
.poll(
async () => {
@@ -332,7 +316,6 @@ test.describe("Connections Page - Search and Filter", () =>
{
)
.toBe(true);
- // Clear search
await connectionsPage.searchConnections("");
const finalCount = await connectionsPage.getConnectionCount();
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/plugins.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/plugins.spec.ts
index 6207a74fec9..7d1face995d 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/plugins.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/plugins.spec.ts
@@ -21,7 +21,6 @@ import { expect, test } from "tests/e2e/fixtures";
test.describe("Plugins Page", () => {
test.beforeEach(async ({ pluginsPage }) => {
await pluginsPage.navigate();
- await pluginsPage.waitForLoad();
});
test("verify plugins page heading is visible", async ({ pluginsPage }) => {
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
index 93574487557..dd97e4dd999 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/pools.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/pools.spec.ts
@@ -16,84 +16,94 @@
* 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";
+import { expect, test } from "tests/e2e/fixtures";
+import { uniqueRunId } from "tests/e2e/utils/test-helpers";
test.describe("Pools Page", () => {
test.setTimeout(60_000);
- let poolsPage: PoolsPage;
- const testPoolName = `test_pool_${Date.now()}`;
+ let testPoolName: string;
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();
+ const createdPoolNames: Array<string> = [];
+
+ test.beforeAll(async ({ authenticatedRequest }) => {
+ testPoolName = `test_pool_${uniqueRunId("pool")}`;
+
+ const response = await authenticatedRequest.post("/api/v2/pools", {
+ data: {
+ description: testPoolDescription,
+ include_deferred: false,
+ name: testPoolName,
+ slots: testPoolSlots,
+ },
+ headers: { "Content-Type": "application/json" },
+ });
+
+ expect([200, 201, 409]).toContain(response.status());
});
- 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.afterAll(async ({ authenticatedRequest }) => {
+ for (const name of [testPoolName, ...createdPoolNames]) {
+ const response = await
authenticatedRequest.delete(`/api/v2/pools/${encodeURIComponent(name)}`);
- test.beforeEach(({ page }) => {
- poolsPage = new PoolsPage(page);
+ if (response.status() !== 404 && !response.ok()) {
+ console.warn(`Pool cleanup failed for ${name}: ${response.status()}`);
+ }
+ }
});
- test("verify pools list displays with default_pool", async () => {
+ test("verify pools list displays with default_pool", async ({ poolsPage })
=> {
await poolsPage.navigate();
await poolsPage.verifyPoolsListDisplays();
});
- test("verify test pool exists in list", async () => {
+ test("verify test pool exists in list", async ({ poolsPage }) => {
await poolsPage.navigate();
await poolsPage.verifyPoolExists(testPoolName);
});
- test("verify pool slots display correctly", async () => {
+ test("verify pool slots display correctly", async ({ poolsPage }) => {
await poolsPage.navigate();
await poolsPage.verifyPoolSlots(testPoolName, testPoolSlots);
});
- test("edit pool slots", async () => {
+ test("edit pool slots", async ({ poolsPage }) => {
await poolsPage.navigate();
await poolsPage.editPoolSlots(testPoolName, updatedSlots);
await poolsPage.navigate();
await poolsPage.verifyPoolSlots(testPoolName, updatedSlots);
});
- test("verify pool usage displays", async () => {
+ test("verify pool usage displays", async ({ poolsPage }) => {
await poolsPage.navigate();
await poolsPage.verifyPoolUsageDisplays("default_pool");
});
- test("create a new pool", async () => {
+ test("create a new pool", async ({ poolsPage }) => {
+ const poolName = `test_crud_pool_${uniqueRunId("crud")}`;
+
+ createdPoolNames.push(poolName);
+
await poolsPage.navigate();
- await poolsPage.createPool(createDeletePoolName, createDeletePoolSlots,
"Created pool");
+ await poolsPage.createPool(poolName, 5, "Created pool");
await poolsPage.navigate();
- await poolsPage.verifyPoolExists(createDeletePoolName);
+ await poolsPage.verifyPoolExists(poolName);
});
- test("delete pool", async () => {
+ test("delete pool", async ({ poolsPage }) => {
+ const poolName = `test_del_pool_${uniqueRunId("del")}`;
+
+ // Create via API so we have a pool to delete via UI
+ await poolsPage.page.request.post("/api/v2/pools", {
+ data: { description: "To be deleted", include_deferred: false, name:
poolName, slots: 5 },
+ headers: { "Content-Type": "application/json" },
+ });
+
await poolsPage.navigate();
- await poolsPage.verifyPoolExists(createDeletePoolName);
- await poolsPage.deletePool(createDeletePoolName);
- await poolsPage.verifyPoolNotExists(createDeletePoolName);
+ await poolsPage.verifyPoolExists(poolName);
+ await poolsPage.deletePool(poolName);
+ await poolsPage.verifyPoolNotExists(poolName);
});
});
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 bee7bd8d12e..c114908f469 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
@@ -21,7 +21,6 @@ import { expect, test } from "tests/e2e/fixtures";
test.describe("Providers Page", () => {
test.beforeEach(async ({ providersPage }) => {
await providersPage.navigate();
- await providersPage.waitForLoad();
});
test("verify providers page heading", async ({ providersPage }) => {
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 e84e09edd9a..477461003c5 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
@@ -16,45 +16,40 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { expect, test } from "@playwright/test";
-import { AUTH_FILE, testConfig } from "playwright.config";
-
-import { RequiredActionsPage } from "../pages/RequiredActionsPage";
+import { testConfig } from "playwright.config";
+import { expect, test } from "tests/e2e/fixtures";
+import { apiDeleteDagRun, setupHITLFlowViaAPI } from
"tests/e2e/utils/test-helpers";
const hitlDagId = testConfig.testDag.hitlId;
+const beforeAllRunIds: Array<string> = [];
+
test.describe("Verify Required Action page", () => {
test.describe.configure({ mode: "serial" });
+ test.slow();
- test.beforeAll(async ({ browser }) => {
- test.setTimeout(400_000);
-
- const context = await browser.newContext({ storageState: AUTH_FILE });
-
- const page = await context.newPage();
- const requiredActionsPage = new RequiredActionsPage(page);
+ test.beforeAll(async ({ authenticatedRequest }) => {
+ test.setTimeout(600_000);
- await requiredActionsPage.runHITLFlowWithApproval(hitlDagId);
- await requiredActionsPage.runHITLFlowWithRejection(hitlDagId);
-
- await context.close();
+ beforeAllRunIds.push(await setupHITLFlowViaAPI(authenticatedRequest,
hitlDagId, true));
+ beforeAllRunIds.push(await setupHITLFlowViaAPI(authenticatedRequest,
hitlDagId, false));
});
- test.fixme("Verify the actions list/table is displayed (or empty state if
none)", async ({ page }) => {
- const browsePage = new RequiredActionsPage(page);
+ test.afterAll(async ({ authenticatedRequest }) => {
+ for (const runId of beforeAllRunIds) {
+ await apiDeleteDagRun(authenticatedRequest, hitlDagId, runId).catch(()
=> undefined);
+ }
+ });
- await browsePage.navigateToRequiredActionsPage();
+ test("Verify the actions table displays with expected columns", async ({
page, requiredActionsPage }) => {
+ await requiredActionsPage.navigateToRequiredActionsPage();
- await
expect(browsePage.actionsTable.or(browsePage.emptyStateMessage)).toBeVisible();
+ await expect(requiredActionsPage.actionsTable).toBeVisible();
- if (await browsePage.actionsTable.isVisible()) {
- await expect(page.locator("th").filter({ hasText: "Dag ID"
})).toBeVisible();
- await expect(page.locator("th").filter({ hasText: "Task ID"
})).toBeVisible();
- await expect(page.locator("th").filter({ hasText: "Dag Run ID"
})).toBeVisible();
- await expect(page.locator("th").filter({ hasText: "Response created at"
})).toBeVisible();
- await expect(page.locator("th").filter({ hasText: "Response received at"
})).toBeVisible();
- } else {
- await expect(browsePage.emptyStateMessage).toBeVisible();
- }
+ await expect(page.locator("th").filter({ hasText: "Dag ID"
})).toBeVisible();
+ await expect(page.locator("th").filter({ hasText: "Task ID"
})).toBeVisible();
+ await expect(page.locator("th").filter({ hasText: "Dag Run ID"
})).toBeVisible();
+ await expect(page.locator("th").filter({ hasText: "Response created at"
})).toBeVisible();
+ await expect(page.locator("th").filter({ hasText: "Response received at"
})).toBeVisible();
});
});
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/variable.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/variable.spec.ts
index d3498ab477b..37b73fe20d9 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/variable.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/variable.spec.ts
@@ -16,14 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { test, expect } from "@playwright/test";
-import { AUTH_FILE } from "playwright.config";
-
-import { VariablePage } from "../pages/VariablePage";
+import { expect, test } from "tests/e2e/fixtures";
+import { apiCreateVariable, apiDeleteVariable, uniqueRunId } from
"tests/e2e/utils/test-helpers";
test.describe("Variables Page", () => {
- let variablesPage: VariablePage;
- let createVariables = 3;
+ test.describe.configure({ mode: "serial" });
const createdVariables: Array<{
description: string;
@@ -31,61 +28,39 @@ test.describe("Variables Page", () => {
value: string;
}> = [];
- test.beforeAll(async ({ browser }) => {
- test.setTimeout(420_000); // for slower browsers
- const context = await browser.newContext({ storageState: AUTH_FILE });
- const page = await context.newPage();
+ const importVarKeys: Array<string> = [];
- variablesPage = new VariablePage(page);
+ test.beforeAll(async ({ authenticatedRequest }) => {
+ test.slow();
- await variablesPage.navigate();
+ const prefix = uniqueRunId("e2e_var");
- for (let i = 0; i < createVariables; i++) {
+ for (let i = 0; i < 3; i++) {
const variable = {
description: `description_${i}`,
- key: `e2e_var_${Date.now()}_${i}_${Math.random().toString(36).slice(2,
8)}`,
+ key: `${prefix}_${i}`,
value: `value_${i}`,
};
createdVariables.push(variable);
-
- // Wait for dialog backdrop to fully disappear
- await expect(page.locator('[data-part="backdrop"]')).toHaveCount(0);
-
- await variablesPage.addButton.click();
-
- await expect(page.getByRole("heading", { name: /add/i })).toBeVisible({
timeout: 20_000 });
-
- await page.getByLabel(/key/i).fill(variable.key);
- await page.getByLabel(/value/i).fill(variable.value);
-
- if (variable.description) {
- await page.getByLabel(/description/i).fill(variable.description);
- }
-
- await page.getByRole("button", { name: /save/i }).click();
-
- await expect(variablesPage.rowByKey(variable.key)).toHaveCount(1);
+ await apiCreateVariable(authenticatedRequest, {
+ description: variable.description,
+ key: variable.key,
+ value: variable.value,
+ });
}
-
- await context.close();
});
- test.beforeEach(async ({ page }) => {
- variablesPage = new VariablePage(page);
- await variablesPage.navigate();
- await variablesPage.waitForLoad();
+ test.beforeEach(async ({ variablePage }) => {
+ await variablePage.navigate();
});
- test("verify variables table displays", async () => {
- await expect(variablesPage.table).toBeVisible();
-
- const rowCount = await variablesPage.tableRows.count();
-
- expect(rowCount).toBeGreaterThan(0);
+ test("verify variables table displays", async ({ variablePage }) => {
+ await expect(variablePage.table).toBeVisible();
+ await expect(variablePage.tableRows).not.toHaveCount(0);
});
- test("verify search filters", async () => {
+ test("verify search filters", async ({ variablePage }) => {
const target = createdVariables.at(0);
expect(target).toBeDefined();
@@ -96,12 +71,12 @@ test.describe("Variables Page", () => {
const targetKey = target.key;
- await variablesPage.search(targetKey);
- await expect(variablesPage.tableRows).toHaveCount(1);
- await expect(variablesPage.rowByKey(targetKey)).toBeVisible();
+ await variablePage.search(targetKey);
+ await expect(variablePage.tableRows).toHaveCount(1);
+ await expect(variablePage.rowByKey(targetKey)).toBeVisible();
});
- test("verify editing a variable", async ({ page }) => {
+ test("verify editing a variable", async ({ page, variablePage }) => {
const target = createdVariables.at(1);
expect(target).toBeDefined();
@@ -110,20 +85,24 @@ test.describe("Variables Page", () => {
throw new Error("No variable available for edit test");
}
- await variablesPage.rowByKey(target.key).getByRole("button", { name:
/edit/i }).click();
+ await variablePage.rowByKey(target.key).getByRole("button", { name:
/edit/i }).click();
await expect(page.getByRole("heading", { name: /edit/i })).toBeVisible();
- await page.getByLabel(/description/i).fill("updated via e2e");
+ const descriptionInput = page.getByLabel(/description/i);
+
+ await descriptionInput.fill("updated via e2e");
+ await expect(descriptionInput).toHaveValue("updated via e2e");
+
await page.getByRole("button", { name: /save/i }).click();
- await variablesPage.waitForLoad();
+ await variablePage.waitForLoad();
- await expect(variablesPage.rowByKey(target.key)).toContainText("updated
via e2e");
+ await expect(variablePage.rowByKey(target.key)).toContainText("updated via
e2e");
});
- test("verify deleting the variable", async ({ page }) => {
- const targetKey = `delete_test_${Date.now()}`;
+ test("verify deleting the variable", async ({ page, variablePage }) => {
+ const targetKey = uniqueRunId("delete_test");
const response = await page.request.post("/api/v2/variables", {
data: {
@@ -136,14 +115,13 @@ test.describe("Variables Page", () => {
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(201);
- await variablesPage.navigate();
- await variablesPage.waitForLoad();
+ await variablePage.navigate();
- await variablesPage.search(targetKey);
- await expect(variablesPage.tableRows).toHaveCount(1, { timeout: 10_000 });
- await expect(variablesPage.rowByKey(targetKey)).toHaveCount(1, { timeout:
10_000 });
+ await variablePage.search(targetKey);
+ await expect(variablePage.tableRows).toHaveCount(1, { timeout: 10_000 });
+ await expect(variablePage.rowByKey(targetKey)).toHaveCount(1, { timeout:
10_000 });
- await variablesPage.selectRow(targetKey);
+ await variablePage.selectRow(targetKey);
await page.getByRole("button", { name: /^delete$/i }).click();
const dialog = page.getByRole("dialog");
@@ -156,11 +134,14 @@ test.describe("Variables Page", () => {
await dialog.getByRole("button", { name: /yes,\s*delete/i }).click();
- await expect(variablesPage.rowByKey(targetKey)).toHaveCount(0);
+ await expect(variablePage.rowByKey(targetKey)).toHaveCount(0);
});
- test("verify importing variables using Import Variables button", async ({
page }) => {
- const uniqueId = `${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
+ test("verify importing variables using Import Variables button", async ({
page, variablePage }) => {
+ const uniqueId = uniqueRunId("import");
+
+ // Pre-register before UI interactions so afterAll cleans up even on test
failure.
+ importVarKeys.push(`import_var_${uniqueId}_1`, `import_var_${uniqueId}_2`);
const importPayload = {
[`import_var_${uniqueId}_1`]: {
@@ -173,7 +154,7 @@ test.describe("Variables Page", () => {
},
};
- await variablesPage.page.getByRole("button", { name: /import variables/i
}).click();
+ await variablePage.page.getByRole("button", { name: /import variables/i
}).click();
const dialog = page.getByRole("dialog");
@@ -189,10 +170,10 @@ test.describe("Variables Page", () => {
await dialog.getByRole("button", { name: /import/i }).click();
- await variablesPage.waitForLoad();
+ await variablePage.waitForLoad();
for (const key of Object.keys(importPayload)) {
- await expect(variablesPage.rowByKey(key)).toHaveCount(1);
+ await expect(variablePage.rowByKey(key)).toHaveCount(1);
const variable = importPayload[key];
@@ -208,59 +189,22 @@ test.describe("Variables Page", () => {
}
});
- test.afterAll(async ({ browser }) => {
- test.setTimeout(300_000);
- if (createdVariables.length === 0) {
- return;
- }
-
- const context = await browser.newContext({ storageState: AUTH_FILE });
- const page = await context.newPage();
+ test.afterAll(async ({ authenticatedRequest }) => {
+ test.slow();
- variablesPage = new VariablePage(page);
-
- await variablesPage.navigate();
- await variablesPage.waitForLoad();
-
- const keysToDelete: Array<string> = [];
-
- for (const variable of createdVariables) {
- const row = variablesPage.rowByKey(variable.key);
-
- if ((await row.count()) > 0) {
- await variablesPage.selectRow(variable.key);
- keysToDelete.push(variable.key);
- }
- }
-
- if (keysToDelete.length === 0) {
- await context.close();
+ // Merge all tracked keys: some may not be in createdVariables if a test
failed early.
+ const allKeys = [...new Set([...createdVariables.map((v) => v.key),
...importVarKeys])];
+ if (allKeys.length === 0) {
return;
}
- await page.getByRole("button", { name: /^delete$/i }).click();
-
- const dialog = page.getByRole("dialog");
-
- await expect(
- dialog.getByRole("heading", {
- name: new RegExp(`delete\\s+${keysToDelete.length}\\s+variable`, "i"),
- }),
- ).toBeVisible();
-
- const codeBlock = dialog.locator("code");
-
- for (const key of keysToDelete) {
- await expect(codeBlock).toContainText(key);
- }
-
- await dialog.getByRole("button", { name: /yes,\s*delete/i }).click();
-
- for (const key of keysToDelete) {
- await expect(variablesPage.rowByKey(key)).toHaveCount(0);
+ for (const key of allKeys) {
+ try {
+ await apiDeleteVariable(authenticatedRequest, key);
+ } catch {
+ // Ignore 404 or other errors - variable may not have been created
+ }
}
-
- await context.close();
});
});
diff --git a/airflow-core/src/airflow/ui/tests/e2e/utils/test-helpers.ts
b/airflow-core/src/airflow/ui/tests/e2e/utils/test-helpers.ts
index 82e3d641b2d..178a2f3caa0 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/utils/test-helpers.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/utils/test-helpers.ts
@@ -502,14 +502,8 @@ export async function apiDeleteDagRun(source: RequestLike,
dagId: string, runId:
* running tasks during deletion, then delete with one retry on timeout.
*/
export async function safeCleanupDagRun(source: RequestLike, dagId: string,
runId: string): Promise<void> {
- const request = getRequestContext(source);
-
try {
- await request.patch(`${baseUrl}/api/v2/dags/${dagId}/dagRuns/${runId}`, {
- data: { state: "failed" },
- headers: { "Content-Type": "application/json" },
- timeout: 10_000,
- });
+ await apiSetDagRunState(source, { dagId, runId, state: "failed" });
} catch {
// Run may already be terminal or deleted — ignore.
}