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 07544ae600e Add E2E test to verify grid view displays correctly on DAG 
detail page #59539 (#60856)
07544ae600e is described below

commit 07544ae600e922a735e79cd5a86a3adaef8cff54
Author: Haseeb Malik <[email protected]>
AuthorDate: Wed Jan 28 01:20:28 2026 -0500

    Add E2E test to verify grid view displays correctly on DAG detail page 
#59539 (#60856)
---
 .../ui/src/components/ui/ButtonGroupToggle.tsx     |   2 +
 .../airflow/ui/src/layouts/Details/Grid/GridTI.tsx |   1 +
 .../ui/src/layouts/Details/PanelButtons.tsx        |   7 +-
 .../src/airflow/ui/tests/e2e/pages/GridPage.ts     | 113 +++++++++++++++++++++
 .../ui/tests/e2e/specs/dag-grid-view.spec.ts       |  94 +++++++++++++++++
 5 files changed, 216 insertions(+), 1 deletion(-)

diff --git 
a/airflow-core/src/airflow/ui/src/components/ui/ButtonGroupToggle.tsx 
b/airflow-core/src/airflow/ui/src/components/ui/ButtonGroupToggle.tsx
index 869656df801..99b4b4a2826 100644
--- a/airflow-core/src/airflow/ui/src/components/ui/ButtonGroupToggle.tsx
+++ b/airflow-core/src/airflow/ui/src/components/ui/ButtonGroupToggle.tsx
@@ -21,6 +21,7 @@ import { Button, ButtonGroup, IconButton } from 
"@chakra-ui/react";
 import type { FC, ReactNode } from "react";
 
 export type ButtonGroupOption<T extends string = string> = {
+  readonly dataTestId?: string;
   readonly disabled?: boolean;
   readonly label: ((isSelected: boolean) => ReactNode) | ReactNode;
   readonly title?: string;
@@ -52,6 +53,7 @@ export const ButtonGroupToggle = <T extends string = string>({
         return (
           <ButtonComponent
             aria-label={option.title}
+            data-testid={option.dataTestId}
             disabled={option.disabled}
             key={option.value}
             onClick={() => onChange(option.value)}
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
index d00c323a3b7..dd5280ea9be 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
@@ -120,6 +120,7 @@ export const GridTI = ({ dagId, instance, isGroup, 
isMapped, onClick, runId, tas
             alignItems="center"
             borderRadius={4}
             colorPalette={instance.state ?? "none"}
+            data-testid="task-state-badge"
             display="flex"
             height="14px"
             justifyContent="center"
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
index 8e2b162f8bc..d5d62c9f0c8 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
@@ -207,7 +207,12 @@ export const PanelButtons = ({
 
   const dagViewOptions: Array<ButtonGroupOption<"graph" | "grid">> = useMemo(
     () => [
-      { label: <FiGrid />, title: 
translate("dag:panel.buttons.showGridShortcut"), value: "grid" },
+      {
+        dataTestId: "grid-view-button",
+        label: <FiGrid />,
+        title: translate("dag:panel.buttons.showGridShortcut"),
+        value: "grid",
+      },
       {
         label: <MdOutlineAccountTree />,
         title: translate("dag:panel.buttons.showGraphShortcut"),
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/GridPage.ts 
b/airflow-core/src/airflow/ui/tests/e2e/pages/GridPage.ts
new file mode 100644
index 00000000000..3e5a1a643bf
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/GridPage.ts
@@ -0,0 +1,113 @@
+/*!
+ * 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 GridPage extends BasePage {
+  public readonly gridCells: Locator;
+  public readonly gridViewButton: Locator;
+  public readonly taskNameLinks: Locator;
+
+  public constructor(page: Page) {
+    super(page);
+    this.gridViewButton = page.getByTestId("grid-view-button");
+    this.gridCells = page.locator('a[id^="grid-"]');
+    this.taskNameLinks = page.locator('a[href*="/tasks/"]');
+  }
+
+  public async clickGridCellAndVerifyDetails(): Promise<void> {
+    await this.waitForGridToLoad();
+
+    const firstCell = this.gridCells.first();
+
+    await expect(firstCell).toBeVisible();
+    await firstCell.click();
+    await this.page.waitForURL(/.*\/tasks\/.*/, { timeout: 15_000 });
+    await expect(this.page.getByTestId("virtualized-list")).toBeVisible({ 
timeout: 10_000 });
+  }
+
+  public async getGridCellCount(): Promise<number> {
+    await this.waitForGridToLoad();
+
+    return this.gridCells.count();
+  }
+
+  public async getTaskNames(): Promise<Array<string>> {
+    await this.waitForGridToLoad();
+
+    const names = await this.taskNameLinks.allTextContents();
+    const uniqueNames = [...new Set(names.map((name) => 
name.trim()).filter((name) => name !== ""))];
+
+    return uniqueNames;
+  }
+
+  public async navigateToDag(dagId: string): Promise<void> {
+    await this.navigateTo(`/dags/${dagId}`);
+    await this.page.waitForURL(`**/dags/${dagId}**`, { timeout: 15_000 });
+    await expect(this.gridViewButton).toBeVisible({ timeout: 10_000 });
+  }
+
+  public async switchToGridView(): Promise<void> {
+    await expect(this.gridViewButton).toBeVisible({ timeout: 10_000 });
+    await this.gridViewButton.click();
+    await this.waitForGridToLoad();
+  }
+
+  public async verifyGridViewIsActive(): Promise<void> {
+    await expect(this.gridViewButton).toBeVisible({ timeout: 10_000 });
+    await expect(this.gridCells.first()).toBeVisible({ timeout: 15_000 });
+    await expect(this.taskNameLinks.first()).toBeVisible({ timeout: 10_000 });
+  }
+
+  public async verifyTaskStatesAreColorCoded(): Promise<void> {
+    await this.waitForGridToLoad();
+
+    const firstCell = this.gridCells.first();
+
+    await expect(firstCell).toBeVisible();
+
+    const badge = firstCell.getByTestId("task-state-badge");
+
+    await expect(badge).toBeVisible();
+
+    const bgColor = await badge.evaluate((el) => 
window.getComputedStyle(el).backgroundColor);
+
+    const isTransparent = !bgColor || bgColor === "transparent" || bgColor === 
"rgba(0, 0, 0, 0)";
+
+    expect(isTransparent).toBe(false);
+  }
+
+  public async verifyTaskTooltipOnHover(): Promise<void> {
+    await this.waitForGridToLoad();
+
+    const firstCell = this.gridCells.first();
+
+    await expect(firstCell).toBeVisible();
+    await firstCell.hover();
+
+    const tooltip = this.page.locator('[role="tooltip"], 
[data-scope="tooltip"]');
+
+    await expect(tooltip.first()).toBeVisible({ timeout: 10_000 });
+  }
+
+  public async waitForGridToLoad(): Promise<void> {
+    await expect(this.gridCells.first()).toBeVisible({ timeout: 20_000 });
+    await expect(this.taskNameLinks.first()).toBeVisible({ timeout: 10_000 });
+  }
+}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-grid-view.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-grid-view.spec.ts
new file mode 100644
index 00000000000..15d0191a7c5
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-grid-view.spec.ts
@@ -0,0 +1,94 @@
+/*!
+ * 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 { AUTH_FILE, testConfig } from "playwright.config";
+import { DagsPage } from "tests/e2e/pages/DagsPage";
+import { GridPage } from "tests/e2e/pages/GridPage";
+
+test.describe("DAG Grid View", () => {
+  let gridPage: GridPage;
+  const testDagId = testConfig.testDag.id;
+
+  test.beforeAll(async ({ browser }) => {
+    test.setTimeout(3 * 60 * 1000);
+    const context = await browser.newContext({ storageState: AUTH_FILE });
+    const page = await context.newPage();
+    const setupDagsPage = new DagsPage(page);
+
+    const dagRunId = await setupDagsPage.triggerDag(testDagId);
+
+    if (dagRunId !== null) {
+      await setupDagsPage.verifyDagRunStatus(testDagId, dagRunId);
+    }
+
+    await context.close();
+  });
+
+  test.beforeEach(({ page }) => {
+    gridPage = new GridPage(page);
+  });
+
+  test("navigate to DAG detail page and display grid view", async () => {
+    test.setTimeout(60_000);
+
+    await gridPage.navigateToDag(testDagId);
+    await gridPage.switchToGridView();
+    await gridPage.verifyGridViewIsActive();
+  });
+
+  test("render grid with task instances", async () => {
+    test.setTimeout(60_000);
+
+    await gridPage.navigateToDag(testDagId);
+    await gridPage.switchToGridView();
+    await gridPage.waitForGridToLoad();
+
+    const taskNames = await gridPage.getTaskNames();
+
+    expect(taskNames.length).toBeGreaterThan(0);
+
+    const cellCount = await gridPage.getGridCellCount();
+
+    expect(cellCount).toBeGreaterThan(0);
+  });
+
+  test("display task states with color coding", async () => {
+    test.setTimeout(60_000);
+
+    await gridPage.navigateToDag(testDagId);
+    await gridPage.switchToGridView();
+    await gridPage.verifyTaskStatesAreColorCoded();
+  });
+
+  test("show task details when clicking a grid cell", async () => {
+    test.setTimeout(60_000);
+
+    await gridPage.navigateToDag(testDagId);
+    await gridPage.switchToGridView();
+    await gridPage.clickGridCellAndVerifyDetails();
+  });
+
+  test("show tooltip on grid cell hover", async () => {
+    test.setTimeout(60_000);
+
+    await gridPage.navigateToDag(testDagId);
+    await gridPage.switchToGridView();
+    await gridPage.verifyTaskTooltipOnHover();
+  });
+});

Reply via email to