diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx index a64ac790ea8d3..ea91e34309bb0 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx @@ -51,6 +51,18 @@ export const CalendarCell = ({ const hasData = Boolean(cellData && relevantCount > 0); const hasTooltip = Boolean(cellData); + // States present in this cell, computed with the same view-mode-aware logic the + // tooltip uses (see CalendarTooltip). Exposed as a `data-states` attribute so e2e + // tests can read run states from the DOM instead of hovering — the tooltip does + // not open reliably under synthetic pointer events in headless Firefox. + const runStates = cellData + ? Object.entries(cellData.counts) + .filter( + ([key, value]) => key !== "total" && value > 0 && (viewMode === "failed" ? key === "failed" : true), + ) + .map(([key]) => key) + : []; + const isMixedState = typeof backgroundColor === "object" && "planned" in backgroundColor && "actual" in backgroundColor; @@ -60,6 +72,7 @@ export const CalendarCell = ({ borderRadius="2px" cursor={hasData ? "pointer" : "default"} data-has-data={hasData ? "true" : "false"} + data-states={runStates.join(" ")} data-testid="calendar-cell" data-view-mode={viewMode} height="14px" @@ -90,6 +103,7 @@ export const CalendarCell = ({ borderRadius="2px" cursor={hasData ? "pointer" : "default"} data-has-data={hasData ? "true" : "false"} + data-states={runStates.join(" ")} data-testid="calendar-cell" data-view-mode={viewMode} height="14px" diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagCalendarTab.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/DagCalendarTab.ts index 77b6d4658644d..74e46a1802df2 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagCalendarTab.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagCalendarTab.ts @@ -33,10 +33,6 @@ export class DagCalendarTab extends BasePage { return this.page.getByTestId("calendar-cell"); } - public get tooltip(): Locator { - return this.page.getByTestId("calendar-tooltip"); - } - public constructor(page: Page) { super(page); @@ -92,22 +88,18 @@ export class DagCalendarTab extends BasePage { const count = await this.activeCells.count(); const states: Array = []; + // Read run states from the cell's `data-states` attribute rather than hovering to + // read the tooltip. The tooltip (BasicTooltip) opens on a `mouseenter` after a + // 500ms delay and renders through a portal; synthetic pointer events do not open + // it reliably in headless Firefox, which made these tests flaky. `data-states` is + // populated with the same view-mode-aware logic the tooltip uses (see + // CalendarCell), so the assertions are unchanged and now backend-independent. for (let i = 0; i < count; i++) { - const cell = this.activeCells.nth(i); + const raw = (await this.activeCells.nth(i).getAttribute("data-states")) ?? ""; - // Firefox sometimes fails to trigger tooltips on hover. - // Retry the hover + tooltip visibility check to handle this. - let text = ""; - - await expect(async () => { - await cell.hover({ force: true }); - await expect(this.tooltip).toBeVisible({ timeout: 3000 }); - text = ((await this.tooltip.textContent()) ?? "").toLowerCase(); - }).toPass({ intervals: [1000], timeout: 20_000 }); - - if (text.includes("success")) states.push("success"); - if (text.includes("failed")) states.push("failed"); - if (text.includes("running")) states.push("running"); + for (const state of raw.split(" ").filter(Boolean)) { + states.push(state); + } } return states; diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-calendar-tab.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-calendar-tab.spec.ts index d05f4b9e9bcbd..771f7241f8501 100644 --- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-calendar-tab.spec.ts +++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-calendar-tab.spec.ts @@ -48,7 +48,7 @@ test.describe("Dag Calendar Tab", () => { expect(states.length).toBeGreaterThanOrEqual(2); }); - test("verify hover shows correct run states", async ({ dagCalendarTab }) => { + test("verify success and failed run states are detected", async ({ dagCalendarTab }) => { await dagCalendarTab.switchToHourly(); const states = await dagCalendarTab.getManualRunStates();