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 f6d294838e8 Add E2E tests for dashboard metrics display (#59678)
f6d294838e8 is described below
commit f6d294838e8e34df73d3b26d46884cc06014774a
Author: Vinod Bottu <[email protected]>
AuthorDate: Mon Dec 29 22:16:04 2025 -0600
Add E2E tests for dashboard metrics display (#59678)
Add E2E tests for dashboard metrics display
---
.../src/airflow/ui/tests/e2e/pages/HomePage.ts | 141 +++++++++++++++++++++
.../ui/tests/e2e/specs/home-dashboard.spec.ts | 141 +++++++++++++++++++++
2 files changed, 282 insertions(+)
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/HomePage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/HomePage.ts
new file mode 100644
index 00000000000..4471bf4ddc2
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/HomePage.ts
@@ -0,0 +1,141 @@
+/*!
+ * 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 "tests/e2e/pages/BasePage";
+
+/**
+ * Home/Dashboard Page Object
+ */
+export class HomePage extends BasePage {
+ // Page URLs
+ public static get homeUrl(): string {
+ return "/";
+ }
+
+ public readonly activeDagsCard: Locator;
+ public readonly dagImportErrorsCard: Locator;
+ public readonly dagProcessorHealth: Locator;
+ public readonly dagRunMetrics: Locator;
+ public readonly failedDagsCard: Locator;
+
+ // Health section elements
+ public readonly healthSection: Locator;
+ // Historical Metrics section (recent runs)
+ public readonly historicalMetricsSection: Locator;
+ public readonly metaDatabaseHealth: Locator;
+ // Pool Summary section
+ public readonly poolSummarySection: Locator;
+ public readonly runningDagsCard: Locator;
+
+ public readonly schedulerHealth: Locator;
+
+ // Dashboard Stats elements
+ public readonly statsSection: Locator;
+ public readonly taskInstanceMetrics: Locator;
+ public readonly triggererHealth: Locator;
+
+ public constructor(page: Page) {
+ super(page);
+
+ // Stats cards - using link patterns that match the StatsCard component
+ this.failedDagsCard = page.locator('a[href*="last_dag_run_state=failed"]');
+ this.runningDagsCard =
page.locator('a[href*="last_dag_run_state=running"]');
+ this.activeDagsCard = page.locator('a[href*="paused=false"]');
+ this.dagImportErrorsCard = page.locator('button:has-text("DAG Import
Errors")');
+
+ // Stats section - using role-based selector
+ this.statsSection = page.getByRole("heading", { name: "Stats"
}).locator("..");
+ // Health section - using role-based selector
+ this.healthSection = page.getByRole("heading", { name: "Health"
}).locator("..");
+ this.metaDatabaseHealth = page.getByText("Metadatabase").first();
+ this.schedulerHealth = page.getByText("Scheduler").first();
+ this.triggererHealth = page.getByText("Triggerer").first();
+ this.dagProcessorHealth = page.getByText("DAG Processor").first();
+
+ // Pool Summary section - using role-based selector
+ this.poolSummarySection = page.getByRole("heading", { name: "Pool Summary"
}).locator("..");
+
+ // Historical Metrics section (recent runs) - using role-based selector
+ this.historicalMetricsSection = page.getByRole("heading", { name:
"History" }).locator("..");
+ this.dagRunMetrics = page.getByRole("heading", { name: /dag run/i
}).first();
+ this.taskInstanceMetrics = page.getByRole("heading", { name: /task
instance/i }).first();
+ }
+
+ /**
+ * Get Active DAGs count
+ */
+ public async getActiveDagsCount(): Promise<number> {
+ return this.getStatsCardCount(this.activeDagsCard);
+ }
+
+ /**
+ * Get Failed DAGs count
+ */
+ public async getFailedDagsCount(): Promise<number> {
+ return this.getStatsCardCount(this.failedDagsCard);
+ }
+
+ /**
+ * Get Running DAGs count
+ */
+ public async getRunningDagsCount(): Promise<number> {
+ return this.getStatsCardCount(this.runningDagsCard);
+ }
+
+ /**
+ * Check if DAG Import Errors are displayed (only visible when errors exist)
+ */
+ public async isDagImportErrorsVisible(): Promise<boolean> {
+ try {
+ return await this.dagImportErrorsCard.isVisible();
+ } catch {
+ return false;
+ }
+ }
+
+ /**
+ * Navigate to Home/Dashboard page
+ */
+ public async navigate(): Promise<void> {
+ await this.navigateTo(HomePage.homeUrl);
+ }
+
+ /**
+ * Wait for dashboard to fully load
+ */
+ public async waitForDashboardLoad(): Promise<void> {
+ await this.welcomeHeading.waitFor({ state: "visible", timeout: 30_000 });
+ }
+
+ /**
+ * Get the count from a stats card
+ */
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
+ private async getStatsCardCount(card: Locator): Promise<number> {
+ await card.waitFor({ state: "visible" }); // Fail fast if card doesn't
exist
+ const badgeText = await card.locator("span").first().textContent();
+ const match = badgeText?.match(/\d+/);
+
+ if (!match) {
+ throw new Error("Could not find count in stats card");
+ }
+
+ return parseInt(match[0], 10);
+ }
+}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts
new file mode 100644
index 00000000000..8ebcff00ea5
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts
@@ -0,0 +1,141 @@
+/*!
+ * 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 { testConfig } from "playwright.config";
+import { DagsPage } from "tests/e2e/pages/DagsPage";
+import { HomePage } from "tests/e2e/pages/HomePage";
+
+test.describe("Dashboard Metrics Display", () => {
+ let homePage: HomePage;
+
+ test.beforeEach(({ page }) => {
+ homePage = new HomePage(page);
+ });
+
+ test("should display dashboard stats section with DAG metrics", async () => {
+ await homePage.navigate();
+ await homePage.waitForDashboardLoad();
+
+ // Use Playwright assertions directly for clearer error messages
+ await expect(homePage.statsSection).toBeVisible();
+
+ await expect(homePage.activeDagsCard).toBeVisible();
+ const activeDagsCount = await homePage.getActiveDagsCount();
+
+ expect(activeDagsCount).toBeGreaterThanOrEqual(0);
+
+ await expect(homePage.runningDagsCard).toBeVisible();
+ const runningDagsCount = await homePage.getRunningDagsCount();
+
+ expect(runningDagsCount).toBeGreaterThanOrEqual(0);
+
+ await expect(homePage.failedDagsCard).toBeVisible();
+ const failedDagsCount = await homePage.getFailedDagsCount();
+
+ expect(failedDagsCount).toBeGreaterThanOrEqual(0);
+ });
+
+ test("should display health status badges", async () => {
+ await homePage.navigate();
+ await homePage.waitForDashboardLoad();
+
+ // Use Playwright assertions directly for clearer error messages
+ await expect(homePage.healthSection).toBeVisible();
+ await expect(homePage.metaDatabaseHealth).toBeVisible();
+ await expect(homePage.schedulerHealth).toBeVisible();
+ await expect(homePage.triggererHealth).toBeVisible();
+ });
+
+ test("should navigate to filtered DAGs list when clicking stats cards",
async () => {
+ await homePage.navigate();
+ await homePage.waitForDashboardLoad();
+
+ await homePage.activeDagsCard.click();
+ await homePage.page.waitForURL(/paused=false/);
+
+ expect(homePage.page.url()).toContain("paused=false");
+
+ await homePage.navigate();
+ await homePage.waitForDashboardLoad();
+
+ await homePage.runningDagsCard.click();
+ await homePage.page.waitForURL(/last_dag_run_state=running/);
+
+ expect(homePage.page.url()).toContain("last_dag_run_state=running");
+ });
+
+ test("should display welcome heading on dashboard", async () => {
+ await homePage.navigate();
+ await homePage.waitForDashboardLoad();
+
+ await expect(homePage.welcomeHeading).toBeVisible();
+ });
+
+ test("should update metrics when DAG is triggered", async () => {
+ // Increase timeout for this test since DAG triggering takes time
+ test.setTimeout(7 * 60 * 1000);
+
+ await homePage.navigate();
+ await homePage.waitForDashboardLoad();
+
+ const initialRunningCount = await homePage.getRunningDagsCount();
+
+ // Trigger a DAG to update metrics
+ const dagsPage = new DagsPage(homePage.page);
+
+ await dagsPage.triggerDag(testConfig.testDag.id);
+
+ // Navigate back to home and verify metrics updated
+ await homePage.navigate();
+ await homePage.waitForDashboardLoad();
+
+ // Verify stats section is still visible after DAG trigger
+ await expect(homePage.statsSection).toBeVisible();
+
+ // Get updated counts - running count should reflect the triggered DAG
+ const updatedRunningCount = await homePage.getRunningDagsCount();
+
+ // Either running count increased or stayed same (if DAG completed quickly)
+ expect(updatedRunningCount).toBeGreaterThanOrEqual(0);
+ expect(initialRunningCount).toBeGreaterThanOrEqual(0);
+ });
+
+ test("should display historical metrics section with recent runs", async ()
=> {
+ await homePage.navigate();
+ await homePage.waitForDashboardLoad();
+
+ // Use Playwright assertions directly for clearer error messages
+ await expect(homePage.historicalMetricsSection).toBeVisible();
+ await expect(homePage.dagRunMetrics).toBeVisible();
+ await expect(homePage.taskInstanceMetrics).toBeVisible();
+ });
+
+ test("should handle DAG import errors display when errors exist", async ()
=> {
+ await homePage.navigate();
+ await homePage.waitForDashboardLoad();
+
+ // DAG Import Errors button only appears when there are actual import
errors
+ const isDagImportErrorsVisible = await homePage.isDagImportErrorsVisible();
+
+ // Skip test with clear message if no import errors exist in the test
environment
+ test.skip(!isDagImportErrorsVisible, "No DAG import errors present in test
environment");
+
+ await expect(homePage.dagImportErrorsCard).toBeVisible();
+ });
+});