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}