This is an automated email from the ASF dual-hosted git repository.

bbovenzi 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 9c62e762dab Add more comprehensive tests on DagCards (#55904)
9c62e762dab is described below

commit 9c62e762dab47d2ee98f5c6460fa9967025932d2
Author: Stuart Buckingham <[email protected]>
AuthorDate: Tue Sep 30 05:53:42 2025 -0700

    Add more comprehensive tests on DagCards (#55904)
    
    * Add labels and test-ids
    
    * Linter
    
    * Fix TypeScript non-null assertion warning in DagCard test
    
    - Replace non-null assertion with proper null check and array destructuring
    - Add explicit error handling for missing mock data
    - Add Jest DOM types import for TypeScript compatibility
    - Maintains test functionality while following linting rules
    
    * Force GMT timezone on timestamps
---
 .../src/airflow/ui/src/components/DagRunInfo.tsx   |   4 +-
 .../airflow/ui/src/pages/DagsList/DagCard.test.tsx | 151 +++++++++++++++++++--
 .../src/airflow/ui/src/pages/DagsList/DagCard.tsx  |  10 +-
 3 files changed, 152 insertions(+), 13 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx 
b/airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx
index 83d17de18ee..0fa1cab7658 100644
--- a/airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx
@@ -70,7 +70,9 @@ const DagRunInfo = ({ endDate, logicalDate, runAfter, 
startDate, state }: Props)
     >
       <Box>
         <Time datetime={runAfter} mr={2} showTooltip={false} />
-        {state === undefined ? undefined : <StateBadge state={state} />}
+        {state === undefined ? undefined : (
+          <StateBadge aria-label={state} data-testid="state-badge" 
state={state} />
+        )}
       </Box>
     </Tooltip>
   );
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
index 9303a5f12c5..319095119bd 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
@@ -18,16 +18,43 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import "@testing-library/jest-dom/vitest";
 import { render, screen } from "@testing-library/react";
 import i18n from "i18next";
 import type { DagTagResponse, DAGWithLatestDagRunsResponse } from 
"openapi-gen/requests/types.gen";
+import type { PropsWithChildren } from "react";
+import { MemoryRouter } from "react-router-dom";
 import { afterEach, describe, it, vi, expect, beforeAll } from "vitest";
 
-import { Wrapper } from "src/utils/Wrapper";
+import { TimezoneProvider } from "src/context/timezone";
+import { BaseWrapper } from "src/utils/Wrapper";
 
 import "../../i18n/config";
 import { DagCard } from "./DagCard";
 
+// Mock the timezone context to always return UTC/GMT
+vi.mock("src/context/timezone", async () => {
+  const actual = await vi.importActual("src/context/timezone");
+
+  return {
+    ...actual,
+    TimezoneProvider: ({ children }: PropsWithChildren) => children,
+    useTimezone: () => ({
+      selectedTimezone: "UTC",
+      setSelectedTimezone: vi.fn(),
+    }),
+  };
+});
+
+// Custom wrapper that uses GMT timezone
+const GMTWrapper = ({ children }: PropsWithChildren) => (
+  <BaseWrapper>
+    <MemoryRouter>
+      <TimezoneProvider>{children}</TimezoneProvider>
+    </MemoryRouter>
+  </BaseWrapper>
+);
+
 const mockDag = {
   asset_expression: null,
   bundle_name: "dags-folder",
@@ -44,20 +71,65 @@ const mockDag = {
   last_expired: null,
   last_parse_duration: 0.23,
   last_parsed_time: "2024-08-22T13:50:10.372238+00:00",
-  latest_dag_runs: [],
+  latest_dag_runs: [
+    {
+      bundle_version: "2025-09-19T18:07:12.9148295Z",
+      conf: {},
+      dag_display_name: "nested_groups",
+      dag_id: "nested_groups",
+      dag_run_id: "scheduled__2025-09-19T19:22:00+00:00",
+      dag_versions: [],
+      data_interval_end: "2025-09-19T19:22:00Z",
+      data_interval_start: "2025-09-19T19:22:00Z",
+      duration: 16.244,
+      end_date: "2025-09-19T19:22:00.798715Z",
+      last_scheduling_decision: "2025-09-19T19:22:00.796419Z",
+      logical_date: "2025-09-19T19:22:00Z",
+      note: null,
+      queued_at: "2025-09-19T19:22:00.762952Z",
+      run_after: "2025-09-19T19:22:00Z",
+      run_type: "scheduled",
+      start_date: "2025-09-19T19:22:00.782471Z",
+      state: "success",
+      triggered_by: null,
+      triggering_user_name: null,
+    },
+    {
+      bundle_version: "2025-09-19T18:07:12.9148295Z",
+      conf: {},
+      dag_display_name: "nested_groups",
+      dag_id: "nested_groups",
+      dag_run_id: "scheduled__2025-09-19T19:21:00+00:00",
+      dag_versions: [],
+      data_interval_end: "2025-09-19T19:21:00Z",
+      data_interval_start: "2025-09-19T19:21:00Z",
+      duration: 16.411,
+      end_date: "2025-09-19T19:21:00.731218Z",
+      last_scheduling_decision: "2025-09-19T19:21:00.728105Z",
+      logical_date: "2025-09-19T19:21:00Z",
+      note: null,
+      queued_at: "2025-09-19T19:21:00.695279Z",
+      run_after: "2025-09-19T19:21:00Z",
+      run_type: "scheduled",
+      start_date: "2025-09-19T19:21:00.714807Z",
+      state: "success",
+      triggered_by: null,
+      triggering_user_name: null,
+    },
+  ],
   max_active_runs: 16,
   max_active_tasks: 16,
   max_consecutive_failed_dag_runs: 0,
   next_dagrun_data_interval_end: "2024-08-23T00:00:00+00:00",
   next_dagrun_data_interval_start: "2024-08-22T00:00:00+00:00",
   next_dagrun_logical_date: "2024-08-22T00:00:00+00:00",
-  next_dagrun_run_after: "2024-08-23T00:00:00+00:00",
+  next_dagrun_run_after: "2024-08-22T19:00:00+00:00",
   owners: ["airflow"],
   pending_actions: [],
   relative_fileloc: "nested_task_groups.py",
   tags: [],
-  timetable_description: "",
-  timetable_summary: "",
+  timetable_description: "Every minute",
+  timetable_summary: "* * * * *",
 } satisfies DAGWithLatestDagRunsResponse;
 
 beforeAll(async () => {
@@ -83,7 +155,7 @@ afterEach(() => {
 
 describe("DagCard", () => {
   it("DagCard should render without tags", () => {
-    render(<DagCard dag={mockDag} />, { wrapper: Wrapper });
+    render(<DagCard dag={mockDag} />, { wrapper: GMTWrapper });
     expect(screen.getByText(mockDag.dag_display_name)).toBeInTheDocument();
     expect(screen.queryByTestId("dag-tag")).toBeNull();
   });
@@ -101,7 +173,8 @@ describe("DagCard", () => {
       tags,
     } satisfies DAGWithLatestDagRunsResponse;
 
-    render(<DagCard dag={expandedMockDag} />, { wrapper: Wrapper });
+    render(<DagCard dag={expandedMockDag} />, { wrapper: GMTWrapper });
+    expect(screen.getByTestId("dag-id")).toBeInTheDocument();
     expect(screen.getByTestId("dag-tag")).toBeInTheDocument();
     expect(screen.queryByText("tag3")).toBeInTheDocument();
     expect(screen.queryByText("tag4")).toBeInTheDocument();
@@ -122,8 +195,70 @@ describe("DagCard", () => {
       tags,
     } satisfies DAGWithLatestDagRunsResponse;
 
-    render(<DagCard dag={expandedMockDag} />, { wrapper: Wrapper });
+    render(<DagCard dag={expandedMockDag} />, { wrapper: GMTWrapper });
+    expect(screen.getByTestId("dag-id")).toBeInTheDocument();
     expect(screen.getByTestId("dag-tag")).toBeInTheDocument();
     expect(screen.getByText("+2 more")).toBeInTheDocument();
   });
+
+  it("DagCard should render schedule section", () => {
+    render(<DagCard dag={mockDag} />, { wrapper: GMTWrapper });
+    const scheduleElement = screen.getByTestId("schedule");
+
+    expect(scheduleElement).toBeInTheDocument();
+    // Should display the timetable summary from mockDag
+    expect(scheduleElement).toHaveTextContent("* * * * *");
+  });
+
+  it("DagCard should render latest run section with actual run data", () => {
+    render(<DagCard dag={mockDag} />, { wrapper: GMTWrapper });
+    const latestRunElement = screen.getByTestId("latest-run");
+
+    expect(latestRunElement).toBeInTheDocument();
+    // Should contain the formatted latest run timestamp (formatted for GMT 
timezone)
+    expect(latestRunElement).toHaveTextContent("2025-09-19 19:22:00");
+  });
+
+  it("DagCard should render next run section with timestamp", () => {
+    render(<DagCard dag={mockDag} />, { wrapper: GMTWrapper });
+    const nextRunElement = screen.getByTestId("next-run");
+
+    expect(nextRunElement).toBeInTheDocument();
+    // Should display the formatted next run timestamp (converted to GMT 
timezone)
+    expect(nextRunElement).toHaveTextContent("2024-08-22 19:00:00");
+  });
+
+  it("DagCard should render StateBadge as success", () => {
+    render(<DagCard dag={mockDag} />, { wrapper: GMTWrapper });
+    const stateBadge = screen.getByTestId("state-badge");
+
+    expect(stateBadge).toBeInTheDocument();
+    // Should have the success state from mockDag.latest_dag_runs[0].state
+    expect(stateBadge).toHaveAttribute("aria-label", "success");
+  });
+
+  it("DagCard should render StateBadge as failed", () => {
+    const [firstDagRun] = mockDag.latest_dag_runs;
+
+    if (!firstDagRun) {
+      throw new Error("Mock data should have at least one dag run");
+    }
+
+    const mockDagWithFailedRun = {
+      ...mockDag,
+      latest_dag_runs: [
+        {
+          ...firstDagRun,
+          state: "failed" as const,
+        },
+      ],
+    } satisfies DAGWithLatestDagRunsResponse;
+
+    render(<DagCard dag={mockDagWithFailedRun} />, { wrapper: GMTWrapper });
+    const stateBadge = screen.getByTestId("state-badge");
+
+    expect(stateBadge).toBeInTheDocument();
+    // Should have the failed state
+    expect(stateBadge).toHaveAttribute("aria-label", "failed");
+  });
 });
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
index bda8b7dc4ec..3d3ddd902ad 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -51,7 +51,9 @@ export const DagCard = ({ dag }: Props) => {
         <HStack>
           <Tooltip content={dag.description} 
disabled={!Boolean(dag.description)}>
             <Link asChild color="fg.info" fontWeight="bold">
-              <RouterLink 
to={`/dags/${dag.dag_id}`}>{dag.dag_display_name}</RouterLink>
+              <RouterLink data-testid="dag-id" to={`/dags/${dag.dag_id}`}>
+                {dag.dag_display_name}
+              </RouterLink>
             </Link>
           </Tooltip>
           <DagTags tags={dag.tags} />
@@ -75,7 +77,7 @@ export const DagCard = ({ dag }: Props) => {
         </HStack>
       </Flex>
       <SimpleGrid columns={4} gap={1} height={20} px={3} py={1}>
-        <Stat label={translate("dagDetails.schedule")}>
+        <Stat data-testid="schedule" label={translate("dagDetails.schedule")}>
           <Schedule
             assetExpression={dag.asset_expression}
             dagId={dag.dag_id}
@@ -84,7 +86,7 @@ export const DagCard = ({ dag }: Props) => {
             timetableSummary={dag.timetable_summary}
           />
         </Stat>
-        <Stat label={translate("dagDetails.latestRun")}>
+        <Stat data-testid="latest-run" 
label={translate("dagDetails.latestRun")}>
           {latestRun ? (
             <Link asChild color="fg.info">
               <RouterLink 
to={`/dags/${latestRun.dag_id}/runs/${latestRun.dag_run_id}`}>
@@ -102,7 +104,7 @@ export const DagCard = ({ dag }: Props) => {
             </Link>
           ) : undefined}
         </Stat>
-        <Stat label={translate("dagDetails.nextRun")}>
+        <Stat data-testid="next-run" label={translate("dagDetails.nextRun")}>
           {Boolean(dag.next_dagrun_run_after) ? (
             <DagRunInfo
               logicalDate={dag.next_dagrun_logical_date}

Reply via email to