This is an automated email from the ASF dual-hosted git repository. kaxilnaik pushed a commit to branch v3-1-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 082e3cfdf642d2c909b3773285f44f535fad85b6 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 (cherry picked from commit 9c62e762dab47d2ee98f5c6460fa9967025932d2) --- .../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}
