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();
+  });
+});

Reply via email to