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 bc96a14f776 UI: Move UI-only e2e checks to unit tests (#74367)
bc96a14f776 is described below
commit bc96a14f77642cac7ea939e0837bf1bf66220f7a
Author: Brent Bovenzi <[email protected]>
AuthorDate: Thu Oct 8 14:27:59 2026 -0400
UI: Move UI-only e2e checks to unit tests (#74367)
* UI: Move UI-only e2e checks to unit tests
The UI e2e jobs spend most of their runtime loading pages and seeding
Dag runs for checks that only exercise client-side rendering, or that
repeat another test against the same component. Covering those in
Vitest and asserting each page's details in a single page load keeps
the Playwright suite focused on flows that need a real Airflow backend,
so the e2e jobs finish sooner and spend less time exposed to runner
instability.
Co-Authored-By: Claude <[email protected]>
* UI: Report every failed display check in merged e2e tests
Each merged display test now asserts a whole page in one load, so a
single failing check would hide the rest. Soft assertions keep the run
going and report every broken cell, while checks that later steps
depend on (rows present, first row visible) still stop the test.
Co-Authored-By: Claude <[email protected]>
* UI: Pin the calendar test clock and drop unused e2e page members
The calendar test derived "this month" in UTC while the component uses
local time, so on machines behind UTC it could fail in the last hours
of a month. The removed e2e tests also left two page-object members
with no callers.
Co-Authored-By: Claude <[email protected]>
* UI: Restore the code scroll e2e check and tighten moved tests
Review found coverage that the move had lost or weakened: the Monaco
scroll check only works in a real browser, the log format checks no
longer saw real task output, the Dag-level required actions route was
no longer rendered anywhere, and the XCom test only asserted a stubbed
prop. Page-object helpers go back to hard assertions so a future caller
cannot continue past a failed check.
Co-Authored-By: Claude <[email protected]>
* UI: Update the moved unit tests for msw 3 and vitest 5
The UI dependency upgrade on main renamed msw's unhandled-request option
and moved jest-dom's matcher types to its vitest entry; the tests added
here predate it.
Co-Authored-By: Claude <[email protected]>
---------
Co-authored-by: Claude <[email protected]>
---
.../DagActions/RunBackfillForm.dateRange.test.tsx | 118 +++++++++++++++++
.../ui/src/components/DataTable/DataTable.test.tsx | 15 ++-
.../Dag/Calendar/Calendar.failedView.test.tsx | 140 +++++++++++++++++++++
.../ui/src/pages/Dag/Calendar/Calendar.test.tsx | 2 +-
.../airflow/ui/src/pages/Dag/Code/Code.test.tsx | 72 +++++++++++
.../ui/src/pages/Dag/RequiredActionsRoute.test.tsx | 54 ++++++++
.../airflow/ui/src/pages/Dag/Tasks/Tasks.test.tsx | 114 +++++++++++++++++
.../ui/src/pages/DagsList/DagsList.test.tsx | 29 +++++
.../ui/src/pages/TaskInstance/Logs/Logs.test.tsx | 62 ++++++++-
.../src/airflow/ui/src/pages/XCom/XCom.test.tsx | 80 ++++++++++++
.../src/airflow/ui/tests/e2e/fixtures/pom.ts | 5 -
.../src/airflow/ui/tests/e2e/pages/BackfillPage.ts | 30 -----
.../airflow/ui/tests/e2e/pages/DagCalendarTab.ts | 48 -------
.../src/airflow/ui/tests/e2e/pages/DagCodePage.ts | 20 ---
.../airflow/ui/tests/e2e/pages/DagDetailPage.ts | 49 --------
.../src/airflow/ui/tests/e2e/pages/DagRunPage.ts | 8 +-
.../src/airflow/ui/tests/e2e/pages/DagRunsPage.ts | 32 -----
.../airflow/ui/tests/e2e/pages/DagRunsTabPage.ts | 33 -----
.../src/airflow/ui/tests/e2e/pages/DagsPage.ts | 115 -----------------
.../src/airflow/ui/tests/e2e/pages/XComsPage.ts | 14 ---
.../src/airflow/ui/tests/e2e/specs/asset.spec.ts | 17 +--
.../airflow/ui/tests/e2e/specs/backfill.spec.ts | 64 ----------
.../airflow/ui/tests/e2e/specs/dag-bundles.spec.ts | 37 ++----
.../ui/tests/e2e/specs/dag-calendar-tab.spec.ts | 75 +----------
.../ui/tests/e2e/specs/dag-code-tab.spec.ts | 8 --
.../airflow/ui/tests/e2e/specs/dag-detail.spec.ts | 43 -------
.../src/airflow/ui/tests/e2e/specs/dag-run.spec.ts | 13 +-
.../ui/tests/e2e/specs/dag-runs-tab.spec.ts | 30 +----
.../airflow/ui/tests/e2e/specs/dag-runs.spec.ts | 10 --
.../airflow/ui/tests/e2e/specs/dag-tasks.spec.ts | 102 ---------------
.../airflow/ui/tests/e2e/specs/dags-list.spec.ts | 67 +---------
.../src/airflow/ui/tests/e2e/specs/plugins.spec.ts | 35 ++----
.../airflow/ui/tests/e2e/specs/providers.spec.ts | 19 ++-
.../ui/tests/e2e/specs/task-instances.spec.ts | 5 -
.../airflow/ui/tests/e2e/specs/task-logs.spec.ts | 66 +---------
.../src/airflow/ui/tests/e2e/specs/xcoms.spec.ts | 20 +--
36 files changed, 726 insertions(+), 925 deletions(-)
diff --git
a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.dateRange.test.tsx
b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.dateRange.test.tsx
new file mode 100644
index 00000000000..51f71430d52
--- /dev/null
+++
b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.dateRange.test.tsx
@@ -0,0 +1,118 @@
+/*!
+ * 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 "@testing-library/jest-dom/vitest";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import dayjs from "dayjs";
+import utc from "dayjs/plugin/utc";
+import { describe, expect, it, vi } from "vitest";
+
+import type * as OpenapiQueries from "openapi/queries";
+
+import { Wrapper } from "src/utils/Wrapper";
+
+import RunBackfillForm from "./RunBackfillForm";
+
+dayjs.extend(utc);
+
+const baseDag = vi.hoisted(() => ({
+ dag_display_name: "Test Dag",
+ dag_id: "test_dag",
+ is_paused: false,
+ max_active_runs: 10,
+ timetable_partitioned: false,
+}));
+
+const rangeError = "Start Date must be before the End Date";
+
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ i18n: { language: "en" },
+ // eslint-disable-next-line id-length
+ t: (key: string) => ({ "backfill.errorStartDateBeforeEndDate": rangeError
})[key] ?? key,
+ }),
+}));
+
+vi.mock("openapi/queries", async (importOriginal) => ({
+ ...(await importOriginal<typeof OpenapiQueries>()),
+ useBackfillServiceListBackfillsUi: vi.fn(() => ({ data: undefined })),
+ useDagRunServiceGetDagRuns: vi.fn(() => ({ data: undefined })),
+ useDagServiceGetDagDetails: vi.fn(() => ({ data: baseDag })),
+}));
+
+vi.mock("src/queries/useCreateBackfillDryRun", () => ({
+ useCreateBackfillDryRun: vi.fn(() => ({ data: undefined, error: undefined,
isPending: false })),
+}));
+
+vi.mock("src/queries/useCreateBackfill", () => ({
+ useCreateBackfill: vi.fn(() => ({
+ createBackfill: vi.fn(),
+ dateValidationError: undefined,
+ error: undefined,
+ isPending: false,
+ resetError: vi.fn(),
+ })),
+}));
+
+vi.mock("src/queries/useDagParams", () => ({ useDagParams: vi.fn(() => ({
paramsDict: {} })) }));
+vi.mock("src/queries/useParamStore", () => ({ useParamStore: vi.fn(() => ({
conf: "{}" })) }));
+vi.mock("src/queries/useTogglePause", () => ({ useTogglePause: vi.fn(() => ({
mutate: vi.fn() })) }));
+vi.mock("src/components/Clear/useRerunWithLatestVersion", () => ({
+ useRerunWithLatestVersion: vi.fn(() => ({ value: false })),
+}));
+vi.mock("../ConfigForm", () => ({ default: () => <div
data-testid="config-form" /> }));
+
+// Same interaction as `BackfillPage.setBound` in the e2e page objects: open a
bound's popover, type a
+// date only, then close it so the next bound's inputs are the only ones with
these placeholders.
+const setBound = async (bound: HTMLElement, date: string) => {
+ fireEvent.click(bound);
+ fireEvent.change(await screen.findByPlaceholderText("YYYY/MM/DD"), { target:
{ value: date } });
+ fireEvent.click(bound);
+ await waitFor(() =>
expect(screen.queryByPlaceholderText("YYYY/MM/DD")).toBeNull());
+};
+
+const renderForm = () => {
+ render(<RunBackfillForm dag={baseDag as never} onClose={vi.fn()} />, {
wrapper: Wrapper });
+
+ const [fromBound, toBound] = screen.getAllByTestId("datetime-input");
+
+ return { fromBound: fromBound as HTMLElement, toBound: toBound as
HTMLElement };
+};
+
+describe("RunBackfillForm date range", () => {
+ it("shows the range error when From is after To", async () => {
+ const { fromBound, toBound } = renderForm();
+
+ await setBound(fromBound, "2025/01/10");
+ await setBound(toBound, "2025/01/01");
+
+ expect(await screen.findByText(rangeError)).toBeVisible();
+ });
+
+ // Regression for #54429: a date-only entry must yield a valid range.
+ it("accepts a date-only range", async () => {
+ const { fromBound, toBound } = renderForm();
+
+ await setBound(fromBound, "2025/01/01");
+ await setBound(toBound, "2025/01/05");
+
+ await waitFor(() => expect(toBound).toHaveTextContent("Jan 05, 2025"));
+ expect(fromBound).toHaveTextContent("Jan 01, 2025");
+ expect(screen.queryByText(rangeError)).toBeNull();
+ });
+});
diff --git
a/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.test.tsx
b/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.test.tsx
index 085e3f84113..ee6861afb31 100644
--- a/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.test.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.test.tsx
@@ -452,21 +452,24 @@ describe("DataTable", () => {
expect(screen.getByLabelText(columnsMenuLabel)).toBeInTheDocument();
});
- it("hides a column when unchecked in the columns menu", async () => {
+ it("hides a column when unchecked in the columns menu and shows it again
when rechecked", async () => {
render(<DataTable columns={wideColumns} data={data} modelName="task"
total={2} />, {
wrapper: ChakraWrapper,
});
- const trigger = screen.getByLabelText(columnsMenuLabel);
+ const table = screen.getByTestId("table-list");
- fireEvent.pointerDown(trigger);
- fireEvent.click(trigger);
+ fireEvent.click(screen.getByLabelText(columnsMenuLabel));
- const menuItem = await waitFor(() => screen.getByRole("menuitem", { name:
"Second" }));
+ const menuItem = await screen.findByRole("menuitem", { name: "Second" });
fireEvent.click(menuItem);
+ await waitFor(() => expect(within(table).queryByRole("columnheader", {
name: "Second" })).toBeNull());
+
expect(within(table).getAllByRole("columnheader")).toHaveLength(wideColumns.length
- 1);
- await waitFor(() =>
expect(within(screen.getByTestId("table-list")).queryByText("Second")).toBeNull());
+ fireEvent.click(menuItem);
+ expect(await within(table).findByRole("columnheader", { name: "Second"
})).toBeInTheDocument();
+
expect(within(table).getAllByRole("columnheader")).toHaveLength(wideColumns.length);
});
// Each slot needs its own entry in the header row condition, or its content
vanishes whenever
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.failedView.test.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.failedView.test.tsx
new file mode 100644
index 00000000000..862065c194a
--- /dev/null
+++
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.failedView.test.tsx
@@ -0,0 +1,140 @@
+/*!
+ * 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 "@testing-library/jest-dom/vitest";
+import { fireEvent, render, screen } from "@testing-library/react";
+import dayjs from "dayjs";
+import { MemoryRouter, Route, Routes } from "react-router-dom";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import type * as OpenapiQueries from "openapi/queries";
+import type { CalendarTimeRangeResponse } from "openapi/requests/types.gen";
+
+import { TIMEZONE_KEY } from "src/constants/localStorage";
+import { TimezoneProvider } from "src/context/timezone";
+import { BaseWrapper } from "src/utils/Wrapper";
+
+import { Calendar } from "./Calendar";
+
+// `calendar-date` pins the displayed month, so the result doesn't depend on
today's date or timezone.
+const dayInMonth = dayjs.utc("2025-03-10T00:00:00Z");
+const hourIso = (hour: number) => dayInMonth.add(hour, "hour").toISOString();
+
+const dagRuns: Array<CalendarTimeRangeResponse> = [
+ { count: 1, date: hourIso(1), state: "success" },
+ { count: 1, date: hourIso(3), state: "failed" },
+ { count: 1, date: hourIso(5), state: "success" },
+ { count: 1, date: hourIso(5), state: "failed" },
+];
+
+vi.mock("openapi/queries", async (importOriginal) => ({
+ ...(await importOriginal<typeof OpenapiQueries>()),
+ useCalendarServiceGetCalendar: vi.fn(() => ({
+ data: { dag_runs: dagRuns, total_entries: dagRuns.length },
+ error: null,
+ isLoading: false,
+ })),
+ useCalendarServiceGetCalendarDeadlines: vi.fn(() => ({ data: { deadlines: []
} })),
+ useDagServiceGetDagDetails: vi.fn(() => ({ data: undefined })),
+}));
+
+const renderCalendar = () =>
+ render(
+ <BaseWrapper>
+ <MemoryRouter
+
initialEntries={["/dags/calendar_dag/calendar?calendar-granularity=hourly&calendar-date=2025-03-10"]}
+ >
+ <TimezoneProvider>
+ <Routes>
+ <Route element={<Calendar />} path="/dags/:dagId/calendar" />
+ </Routes>
+ </TimezoneProvider>
+ </MemoryRouter>
+ </BaseWrapper>,
+ );
+
+const getCells = () => screen.getAllByTestId("calendar-cell");
+
+const getActiveCells = () => getCells().filter((cell) => cell.dataset.hasData
=== "true");
+
+const getStates = (cells: Array<HTMLElement>) =>
+ cells.flatMap((cell) => (cell.dataset.states ?? "").split("
").filter(Boolean));
+
+const findCellIndex = (states: string) => getCells().findIndex((cell) =>
cell.dataset.states === states);
+
+const switchToFailedView = () => fireEvent.click(screen.getByRole("button", {
name: /failed/iu }));
+
+describe("Calendar failed view", () => {
+ beforeEach(() => {
+ localStorage.clear();
+ localStorage.setItem(TIMEZONE_KEY, JSON.stringify("UTC"));
+ });
+
+ it("shows only failed runs and fewer active cells in the failed view", () =>
{
+ renderCalendar();
+
+ expect(screen.getByTestId("calendar-hourly-view")).toBeInTheDocument();
+
+ const totalCells = getActiveCells();
+
+ expect(totalCells).toHaveLength(3);
+ expect(getStates(totalCells)).toEqual(expect.arrayContaining(["success",
"failed"]));
+
+ const successOnlyIndex = findCellIndex("success");
+
+ expect(successOnlyIndex).toBeGreaterThanOrEqual(0);
+
+ switchToFailedView();
+
+ const failedCells = getActiveCells();
+
+ expect(failedCells).toHaveLength(2);
+ expect(getStates(failedCells)).toContain("failed");
+ expect(getStates(failedCells)).not.toContain("success");
+ expect(getCells()[successOnlyIndex]).toHaveAttribute("data-has-data",
"false");
+ });
+
+ it("marks every cell with the failed view mode", () => {
+ renderCalendar();
+
+ for (const cell of getCells()) {
+ expect(cell).toHaveAttribute("data-view-mode", "total");
+ }
+
+ switchToFailedView();
+
+ for (const cell of getCells()) {
+ expect(cell).toHaveAttribute("data-view-mode", "failed");
+ }
+ });
+
+ it("drops the success color from mixed cells in the failed view", () => {
+ renderCalendar();
+
+ const mixedIndex = findCellIndex("failed success");
+
+ expect(mixedIndex).toBeGreaterThanOrEqual(0);
+ // Mixed success/failed cells render as two diagonal color layers.
+ expect(getCells()[mixedIndex]?.children).toHaveLength(2);
+
+ switchToFailedView();
+
+ expect(getCells()[mixedIndex]).toHaveAttribute("data-states", "failed");
+ expect(getCells()[mixedIndex]?.children).toHaveLength(0);
+ });
+});
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.test.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.test.tsx
index 27a7aee414c..937da12ffbc 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.test.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import "@testing-library/jest-dom";
+import "@testing-library/jest-dom/vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import dayjs from "dayjs";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Code/Code.test.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Code/Code.test.tsx
new file mode 100644
index 00000000000..1976febb022
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Code/Code.test.tsx
@@ -0,0 +1,72 @@
+/*!
+ * 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 { render } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import type * as OpenapiQueries from "openapi/queries";
+
+import type { EditorProps } from "src/components/MonacoEditor";
+
+import { Wrapper } from "src/utils/Wrapper";
+
+import { Code } from "./Code";
+
+const source = 'from airflow.sdk import DAG\n\nwith DAG(dag_id="example"):\n
pass\n';
+
+const editorProps = vi.hoisted(() => ({ current: undefined as EditorProps |
undefined }));
+
+vi.mock("src/components/MonacoEditor", () => ({
+ default: (props: EditorProps) => {
+ editorProps.current = props;
+
+ return null;
+ },
+}));
+
+vi.mock("react-router-dom", async () => {
+ const actual = await vi.importActual("react-router-dom");
+
+ return { ...actual, useParams: () => ({ dagId: "example" }) };
+});
+
+vi.mock("openapi/queries", async (importOriginal) => ({
+ ...(await importOriginal<typeof OpenapiQueries>()),
+ useDagRunServiceGetDagRun: vi.fn(() => ({ data: undefined })),
+ useDagServiceGetDagDetails: vi.fn(() => ({ data: undefined, error: null,
isLoading: false })),
+ useDagSourceServiceGetDagSource: vi.fn(() => ({
+ data: { content: source, dag_id: "example", language: "python",
version_number: 1 },
+ error: null,
+ isLoading: false,
+ })),
+ useDagVersionServiceGetDagVersion: vi.fn(() => ({ data: undefined })),
+ useDagVersionServiceGetDagVersions: vi.fn(() => ({ data: { dag_versions: [],
total_entries: 0 } })),
+}));
+
+vi.mock("src/hooks/useSelectedVersion", () => ({ default: vi.fn(() =>
undefined) }));
+vi.mock("src/queries/useConfig", () => ({ useConfig: vi.fn(() => false) }));
+
+describe("Code", () => {
+ it("passes the Dag source, its language and line numbers to the editor", ()
=> {
+ render(<Code />, { wrapper: Wrapper });
+
+ expect(editorProps.current?.value).toBe(source);
+ expect(editorProps.current?.language).toBe("python");
+ expect(editorProps.current?.options?.lineNumbers).toBe("on");
+ });
+});
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/RequiredActionsRoute.test.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/RequiredActionsRoute.test.tsx
new file mode 100644
index 00000000000..3051799786c
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/RequiredActionsRoute.test.tsx
@@ -0,0 +1,54 @@
+/*!
+ * 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 "@testing-library/jest-dom/vitest";
+import { render, screen, waitFor } from "@testing-library/react";
+import { http, HttpResponse } from "msw";
+import { setupServer, type SetupServer } from "msw/node";
+import { afterAll, beforeAll, describe, expect, it } from "vitest";
+
+import { handlers } from "src/mocks/handlers";
+import { AppWrapper } from "src/utils/AppWrapper";
+
+const hitlRequests: Array<{ dagId: string; dagRunId: string }> = [];
+
+let server: SetupServer;
+
+beforeAll(() => {
+ server = setupServer(
+ ...handlers,
+ http.get("/api/v2/dags/:dagId/dagRuns/:dagRunId/hitlDetails", ({ params })
=> {
+ hitlRequests.push({ dagId: String(params.dagId), dagRunId:
String(params.dagRunId) });
+
+ return HttpResponse.json({ hitl_details: [], total_entries: 0 });
+ }),
+ );
+ server.listen({ onUnhandledFrame: "bypass" });
+});
+afterAll(() => server.close());
+
+describe("Dag required actions route", () => {
+ it("opens the HITL review modal scoped to the Dag", async () => {
+ render(<AppWrapper
initialEntries={["/dags/tutorial_taskflow_api/required_actions"]} />);
+
+ expect(await screen.findByRole("dialog")).toBeInTheDocument();
+ await waitFor(() =>
+ expect(hitlRequests).toContainEqual({ dagId: "tutorial_taskflow_api",
dagRunId: "~" }),
+ );
+ });
+});
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.test.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.test.tsx
new file mode 100644
index 00000000000..d602fa93f86
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.test.tsx
@@ -0,0 +1,114 @@
+/*!
+ * 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 "@testing-library/jest-dom/vitest";
+import { fireEvent, render, screen, waitFor, within } from
"@testing-library/react";
+import { http, HttpResponse } from "msw";
+import { setupServer, type SetupServer } from "msw/node";
+import { MemoryRouter, Route, Routes } from "react-router-dom";
+import { afterAll, beforeAll, describe, expect, it } from "vitest";
+
+import { BaseWrapper } from "src/utils/Wrapper";
+
+import { Tasks } from "./Tasks";
+
+const tasks = [
+ {
+ is_mapped: false,
+ operator_name: "BashOperator",
+ retries: 0,
+ task_id: "extract",
+ trigger_rule: "all_success",
+ },
+ {
+ is_mapped: false,
+ operator_name: "PythonOperator",
+ retries: 2,
+ task_id: "transform",
+ trigger_rule: "all_success",
+ },
+ {
+ is_mapped: false,
+ operator_name: "PythonOperator",
+ retries: 0,
+ task_id: "load",
+ trigger_rule: "all_done",
+ },
+].map((task) => ({ ...task, task_display_name: task.task_id }));
+
+let server: SetupServer;
+
+beforeAll(() => {
+ server = setupServer(
+ http.get("/api/v2/dags/:dagId/tasks", () => HttpResponse.json({ tasks,
total_entries: tasks.length })),
+ );
+ server.listen({ onUnhandledFrame: "bypass" });
+});
+afterAll(() => server.close());
+
+const getTaskNames = () =>
+ within(screen.getByTestId("table-list"))
+ .queryAllByRole("link")
+ .map((link) => link.textContent);
+
+const renderTasks = async () => {
+ render(
+ <BaseWrapper>
+ <MemoryRouter initialEntries={["/dags/example_dag/tasks"]}>
+ <Routes>
+ <Route element={<Tasks />} path="/dags/:dagId/tasks" />
+ </Routes>
+ </MemoryRouter>
+ </BaseWrapper>,
+ );
+
+ await waitFor(() => expect(getTaskNames()).toEqual(["extract", "transform",
"load"]));
+};
+
+const selectOption = async (filterTestId: string, option: string) => {
+ fireEvent.click(screen.getByTestId(filterTestId));
+ fireEvent.click(await screen.findByRole("option", { name: option }));
+};
+
+describe("Dag tasks filters", () => {
+ it.each([
+ { expected: ["extract"], filterTestId: "operator-filter", option:
"BashOperator" },
+ { expected: ["transform", "load"], filterTestId: "operator-filter",
option: "PythonOperator" },
+ { expected: ["load"], filterTestId: "trigger-rule-filter", option:
"all_done" },
+ { expected: ["extract", "transform"], filterTestId: "trigger-rule-filter",
option: "all_success" },
+ { expected: ["transform"], filterTestId: "retries-filter", option: "2" },
+ { expected: ["extract", "load"], filterTestId: "retries-filter", option:
"0" },
+ ])(
+ "narrows the list to $expected when $filterTestId is $option",
+ async ({ expected, filterTestId, option }) => {
+ await renderTasks();
+
+ await selectOption(filterTestId, option);
+
+ await waitFor(() => expect(getTaskNames()).toEqual(expected));
+ },
+ );
+
+ it("narrows the list to tasks matching the searched name", async () => {
+ await renderTasks();
+
+ fireEvent.change(screen.getByPlaceholderText("searchTasks"), { target: {
value: "trans" } });
+
+ await waitFor(() => expect(getTaskNames()).toEqual(["transform"]));
+ });
+});
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
index 3f618e7e089..51dc04c8a2d 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
@@ -394,3 +394,32 @@ describe("Dags table", () => {
await waitFor(() => expect(cell.firstElementChild).toHaveStyle({
whiteSpace: "nowrap" }));
});
});
+
+describe("Dags display toggle", () => {
+ it("switches between card and table views", async () => {
+ render(<AppWrapper initialEntries={["/dags"]} />);
+
+ const cardList = await screen.findByTestId("card-list");
+
+ await waitFor(() =>
+
expect(within(cardList).getByText("tutorial_taskflow_api_success")).toBeInTheDocument(),
+ );
+ expect(screen.queryByTestId("table-list")).toBeNull();
+
+ fireEvent.click(screen.getByLabelText(/toggleTableView/iu));
+
+ const tableList = await screen.findByTestId("table-list");
+
+
expect(within(tableList).getByText("tutorial_taskflow_api_success")).toBeInTheDocument();
+ expect(screen.queryByTestId("card-list")).toBeNull();
+
+ fireEvent.click(screen.getByLabelText(/toggleCardView/iu));
+
+ await waitFor(() =>
+ expect(
+
within(screen.getByTestId("card-list")).getByText("tutorial_taskflow_api_success"),
+ ).toBeInTheDocument(),
+ );
+ expect(screen.queryByTestId("table-list")).toBeNull();
+ });
+});
diff --git
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx
index b2bf258917f..1ac5a031a74 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx
@@ -18,7 +18,7 @@
*/
import "@testing-library/jest-dom/vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
-import { beforeAll, describe, expect, it } from "vitest";
+import { afterEach, beforeAll, describe, expect, it } from "vitest";
import { AppWrapper } from "src/utils/AppWrapper";
@@ -82,6 +82,66 @@ describe("Task log source", () => {
expect(loc).toHaveProperty("innerText", "loc=dagbag.py:593");
});
});
+
+const timestampPattern = /\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]/u;
+const levelPattern = /INFO - /u;
+
+const selectSetting = async (testId: string) => {
+ fireEvent.click(screen.getByTestId("log-settings-button"));
+ fireEvent.click(await screen.findByTestId(testId));
+};
+
+const renderLogSourceTask = async () => {
+ render(
+ <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/log_source"]}
/>,
+ );
+ await waitForLogs();
+
+ return screen.getByTestId("virtualized-list");
+};
+
+describe("Task log settings", () => {
+ // Settings persist in localStorage, so reset them for the other tests.
+ afterEach(() => localStorage.clear());
+
+ it("renders log levels and formatted timestamps", async () => {
+ const logList = await renderLogSourceTask();
+
+ expect(logList).toHaveTextContent(timestampPattern);
+ expect(logList).toHaveTextContent(levelPattern);
+ });
+
+ it("hides timestamps and log levels and shows the source from the settings
menu", async () => {
+ const logList = await renderLogSourceTask();
+
+ await selectSetting("log-settings-timestamp");
+ await waitFor(() =>
expect(logList).not.toHaveTextContent(timestampPattern));
+ expect(logList).toHaveTextContent(levelPattern);
+
+ await selectSetting("log-settings-log-level");
+ await waitFor(() => expect(logList).not.toHaveTextContent(levelPattern));
+
+ expect(logList.querySelector('[data-key="logger"]')).toBeNull();
+ await selectSetting("log-settings-source");
+ await waitFor(() =>
expect(logList.querySelector('[data-key="logger"]')).not.toBeNull());
+ });
+
+ it("toggles the wrap and expand menu item labels", async () => {
+ await renderLogSourceTask();
+
+ fireEvent.click(screen.getByTestId("log-settings-button"));
+ expect(await
screen.findByTestId("log-settings-wrap")).toHaveTextContent("wrap.wrap");
+
expect(screen.getByTestId("log-settings-expand")).toHaveTextContent("expand.expand");
+
+ fireEvent.click(screen.getByTestId("log-settings-wrap"));
+ await selectSetting("log-settings-expand");
+
+ fireEvent.click(screen.getByTestId("log-settings-button"));
+ await waitFor(() =>
expect(screen.getByTestId("log-settings-wrap")).toHaveTextContent("wrap.unwrap"));
+
expect(screen.getByTestId("log-settings-expand")).toHaveTextContent("expand.collapse");
+ });
+});
+
describe("Task log grouping", () => {
it("Display task log content on click", async () => {
render(
diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XCom.test.tsx
b/airflow-core/src/airflow/ui/src/pages/XCom/XCom.test.tsx
new file mode 100644
index 00000000000..18aab48cd2a
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/XCom/XCom.test.tsx
@@ -0,0 +1,80 @@
+/*!
+ * 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 "@testing-library/jest-dom/vitest";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { http, HttpResponse } from "msw";
+import { setupServer, type SetupServer } from "msw/node";
+import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
+
+import type * as ColorMode from "src/context/colorMode";
+import { handlers } from "src/mocks/handlers";
+import { AppWrapper } from "src/utils/AppWrapper";
+
+vi.mock("src/components/MonacoEditor", () => ({
+ default: ({ value }: { readonly value?: string }) => <div
data-testid="monaco-editor">{value}</div>,
+}));
+
+vi.mock("src/context/colorMode", async (importOriginal) => ({
+ ...(await importOriginal<typeof ColorMode>()),
+ useMonacoTheme: () => ({ beforeMount: vi.fn(), theme: "airflow-light" }),
+}));
+
+const xcomEntry = {
+ dag_display_name: "example_dag",
+ dag_id: "example_dag",
+ key: "return_value",
+ map_index: -1,
+ run_after: "2025-01-01T00:00:00Z",
+ run_id: "manual_run",
+ task_display_name: "push",
+ task_id: "push",
+ timestamp: "2025-01-01T00:00:00Z",
+};
+
+let server: SetupServer;
+
+beforeAll(() => {
+ server = setupServer(
+ ...handlers,
+ http.get("/api/v2/dags/~/dagRuns/~/taskInstances/~/xcomEntries", () =>
+ HttpResponse.json({ total_entries: 1, xcom_entries: [xcomEntry] }),
+ ),
+
http.get("/api/v2/dags/:dagId/dagRuns/:runId/taskInstances/:taskId/xcomEntries/:key",
() =>
+ HttpResponse.json({ ...xcomEntry, value: { nested: { answer: 42 } } }),
+ ),
+ );
+ server.listen({ onUnhandledFrame: "bypass" });
+});
+afterAll(() => server.close());
+
+describe("XCom list expand/collapse", () => {
+ it("expands and collapses XCom values via the expand/collapse all buttons",
async () => {
+ render(<AppWrapper initialEntries={["/xcoms"]} />);
+
+ await waitFor(() =>
expect(screen.getByTestId("expand-all-button")).toBeInTheDocument());
+ await waitFor(() =>
expect(screen.getByText(/nested/u)).toBeInTheDocument());
+ expect(screen.queryByTestId("monaco-editor")).toBeNull();
+
+ fireEvent.click(screen.getByTestId("expand-all-button"));
+ expect(await
screen.findByTestId("monaco-editor")).toHaveTextContent(/"answer": 42/u);
+
+ fireEvent.click(screen.getByTestId("collapse-all-button"));
+ await waitFor(() =>
expect(screen.queryByTestId("monaco-editor")).toBeNull());
+ });
+});
diff --git a/airflow-core/src/airflow/ui/tests/e2e/fixtures/pom.ts
b/airflow-core/src/airflow/ui/tests/e2e/fixtures/pom.ts
index 94a21027f47..c27eed3067b 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/fixtures/pom.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/fixtures/pom.ts
@@ -33,7 +33,6 @@ import { ConnectionsPage } from "../pages/ConnectionsPage";
import { DagBundlesPage } from "../pages/DagBundlesPage";
import { DagCalendarTab } from "../pages/DagCalendarTab";
import { DagCodePage } from "../pages/DagCodePage";
-import { DagDetailPage } from "../pages/DagDetailPage";
import { DagRunPage } from "../pages/DagRunPage";
import { DagRunsPage } from "../pages/DagRunsPage";
import { DagRunsTabPage } from "../pages/DagRunsTabPage";
@@ -63,7 +62,6 @@ export type PomFixtures = {
dagBundlesPage: DagBundlesPage;
dagCalendarTab: DagCalendarTab;
dagCodePage: DagCodePage;
- dagDetailPage: DagDetailPage;
dagRunPage: DagRunPage;
dagRunsPage: DagRunsPage;
dagRunsTabPage: DagRunsTabPage;
@@ -119,9 +117,6 @@ export const test = base.extend<PomFixtures,
PomWorkerFixtures>({
dagCodePage: async ({ page }, use) => {
await use(new DagCodePage(page));
},
- dagDetailPage: async ({ page }, use) => {
- await use(new DagDetailPage(page));
- },
dagRunPage: async ({ page }, use) => {
await use(new DagRunPage(page));
},
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts
index 8c5e8cc4c4f..cf4f7e1bd88 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts
@@ -81,7 +81,6 @@ function getColumnIndex(columnMap: Map<string, number>, name:
string): number {
const DETAIL_COLUMNS = ["From", "To", "Reprocess Behavior", "Created at",
"Completed at"];
export class BackfillPage extends BasePage {
- public readonly backfillDateError: Locator;
public readonly backfillFromTrigger: Locator;
public readonly backfillModeRadio: Locator;
public readonly backfillRunButton: Locator;
@@ -107,7 +106,6 @@ export class BackfillPage extends BasePage {
this.backfillToTrigger = page.getByTestId("datetime-input").nth(1);
this.backfillRunButton = page.getByRole("button", { name: "Run Backfill"
});
this.backfillsTable = page.getByTestId("table-list");
- this.backfillDateError = page.getByText("Start Date must be before the End
Date");
this.cancelButton = page.getByRole("button", { name: "Cancel backfill" });
this.pauseButton = page.getByRole("button", { name: "Pause backfill" });
this.unpauseButton = page.getByRole("button", { name: "Unpause backfill"
});
@@ -366,14 +364,6 @@ export class BackfillPage extends BasePage {
};
}
- public getColumnHeader(columnName: string): Locator {
- return this.backfillsTable.getByRole("columnheader", { name: columnName });
- }
-
- public getFilterButton(): Locator {
- return this.page.getByRole("button", { name: /filter table columns/i });
- }
-
public async navigateToBackfillsTab(dagName: string): Promise<void> {
await expect(async () => {
await this.navigateTo(BackfillPage.getBackfillsUrl(dagName));
@@ -394,22 +384,6 @@ export class BackfillPage extends BasePage {
await expect(this.backfillFromTrigger).toBeVisible();
}
- public async openFilterMenu(): Promise<void> {
- // After Escape the menu content stays mounted and visible with
data-state="closed" until its exit
- // animation ends, which a loaded WebKit can delay for many seconds. Focus
returns to the trigger
- // meanwhile, so its tooltip opens over the stale first item. Only an open
menu counts, so the loop
- // re-clicks the trigger (which also closes the tooltip) instead of
probing the stale content.
- const openMenu = this.page.locator('[role="menu"][data-state="open"]');
-
- await expect(async () => {
- if (!(await openMenu.isVisible())) {
- await this.getFilterButton().click();
- }
- await expect(openMenu).toBeVisible({ timeout: 3000 });
- await openMenu.getByRole("menuitem").first().click({ timeout: 3000,
trial: true });
- }).toPass({ intervals: [1000], timeout: 15_000 });
- }
-
public async pauseBackfillViaApi(backfillId: number): Promise<boolean> {
let isPaused = false;
@@ -481,10 +455,6 @@ export class BackfillPage extends BasePage {
}).toPass({ intervals: [300, 700, 1500], timeout: 15_000 });
}
- public async toggleColumn(columnName: string): Promise<void> {
- await this.page.getByRole("menuitem", { name: columnName }).click();
- }
-
public async togglePauseState(): Promise<void> {
await this.pauseOrUnpauseButton.click();
}
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 7971a41025c..12f46dd1012 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagCalendarTab.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagCalendarTab.ts
@@ -22,7 +22,6 @@ import { BasePage } from "tests/e2e/pages/BasePage";
export class DagCalendarTab extends BasePage {
public readonly dailyToggle: Locator;
- public readonly failedToggle = this.page.getByRole("button", { name:
/failed/i });
public readonly hourlyToggle: Locator;
public readonly totalToggle = this.page.getByRole("button", { name: /total/i
});
@@ -41,46 +40,6 @@ export class DagCalendarTab extends BasePage {
this.dailyToggle = page.getByRole("button", { name: /daily/i });
}
- public async getActiveCellColors(): Promise<Array<string>> {
- const count = await this.activeCells.count();
- const colors: Array<string> = [];
-
- for (let i = 0; i < count; i++) {
- const cell = this.activeCells.nth(i);
- const computedColor = await cell.evaluate((el: Element) => {
- const getRenderableColor = (element: Element): string => {
- const color = window.getComputedStyle(element).backgroundColor;
-
- return color && color !== "rgba(0, 0, 0, 0)" && color !==
"transparent" ? color : "";
- };
-
- const cellColor = getRenderableColor(el);
-
- if (cellColor) {
- return cellColor;
- }
-
- const children = [...el.querySelectorAll("*")];
-
- for (const child of children) {
- const childColor = getRenderableColor(child);
-
- if (childColor) {
- return childColor;
- }
- }
-
- return "";
- });
-
- if (computedColor) {
- colors.push(computedColor);
- }
- }
-
- return colors;
- }
-
public async getActiveCellCount(): Promise<number> {
return this.activeCells.count();
}
@@ -114,13 +73,6 @@ export class DagCalendarTab extends BasePage {
await this.waitForCalendarReady();
}
- public async switchToFailedView(): Promise<void> {
- await this.failedToggle.click();
-
- await expect(this.page).toHaveURL(/calendar-view-mode=failed/u);
- await expect(this.activeCells.first()).toHaveAttribute("data-view-mode",
"failed");
- }
-
public async switchToHourly(): Promise<void> {
await this.hourlyToggle.click();
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagCodePage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagCodePage.ts
index 808ff450409..4be79ebebe3 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagCodePage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagCodePage.ts
@@ -23,16 +23,12 @@ import { BasePage } from "tests/e2e/pages/BasePage";
export class DagCodePage extends BasePage {
public readonly editorContainer: Locator;
public readonly editorScrollable: Locator;
- public readonly lineNumbers: Locator;
- public readonly syntaxTokens: Locator;
public readonly viewLines: Locator;
public constructor(page: Page) {
super(page);
this.editorContainer = page.locator('[role="code"]');
- this.lineNumbers = page.locator(".monaco-editor .margin-view-overlays
.line-numbers");
this.editorScrollable = page.locator(".monaco-scrollable-element");
- this.syntaxTokens = page.locator(".monaco-editor .view-line span span");
this.viewLines = page.locator(".monaco-editor .view-line");
}
@@ -54,14 +50,6 @@ export class DagCodePage extends BasePage {
await expect.poll(async () => scrollable.evaluate((el) => el.scrollHeight
> el.clientHeight)).toBe(true);
}
- public async verifyLineNumbersDisplayed(): Promise<void> {
- await this.waitForCodeReady();
-
- await expect(this.lineNumbers.first()).toBeVisible({ timeout: 30_000 });
- await expect(this.lineNumbers).not.toHaveCount(0);
- await expect(this.lineNumbers.first()).toHaveText("1");
- }
-
public async verifySourceCodeDisplayed(): Promise<void> {
await this.waitForCodeReady();
@@ -69,14 +57,6 @@ export class DagCodePage extends BasePage {
await expect(this.viewLines).not.toHaveCount(0);
}
- public async verifySyntaxHighlighting(): Promise<void> {
- await this.waitForCodeReady();
-
- await expect(this.syntaxTokens.first()).toBeVisible({ timeout: 30_000 });
- await expect(this.syntaxTokens).not.toHaveCount(0);
- await expect(this.syntaxTokens.first()).toHaveAttribute("class", /mtk\d+/);
- }
-
private async waitForCodeReady(): Promise<void> {
await expect(this.editorContainer).toBeVisible({ timeout: 60_000 });
await expect(this.viewLines.first()).toBeVisible({ timeout: 30_000 });
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagDetailPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagDetailPage.ts
deleted file mode 100644
index 281d733bd10..00000000000
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagDetailPage.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/*!
- * 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 type { Locator, Page } from "@playwright/test";
-
-import { HITLReviewModal } from "tests/e2e/components/HITLReviewModal";
-import { BasePage } from "tests/e2e/pages/BasePage";
-
-export class DagDetailPage extends BasePage {
- public readonly hitlReviewModal: HITLReviewModal;
- public readonly requiredActionsButton: Locator;
-
- public constructor(page: Page) {
- super(page);
- this.hitlReviewModal = new HITLReviewModal(page);
- this.requiredActionsButton = page.getByRole("button", { name: "Required
Actions" });
- }
-
- public static getDagDetailRequiredActionsUrl(dagId: string): string {
- return this.getDagDetailUrl(dagId) + `/required_actions`;
- }
-
- public static getDagDetailUrl(dagId: string): string {
- return `/dags/${dagId}`;
- }
-
- public async navigateToDagDetail(dagId: string): Promise<void> {
- await this.navigateTo(DagDetailPage.getDagDetailUrl(dagId));
- }
-
- public async navigateToDagDetailRequiredActions(dagId: string):
Promise<void> {
- await this.navigateTo(DagDetailPage.getDagDetailRequiredActionsUrl(dagId));
- }
-}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunPage.ts
index 0c9f9065668..5c3708a32f3 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunPage.ts
@@ -16,19 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-import type { Locator, Page } from "@playwright/test";
+import type { Page } from "@playwright/test";
import { HITLReviewModal } from "tests/e2e/components/HITLReviewModal";
import { BasePage } from "tests/e2e/pages/BasePage";
export class DagRunPage extends BasePage {
public readonly hitlReviewModal: HITLReviewModal;
- public readonly requiredActionsButton: Locator;
public constructor(page: Page) {
super(page);
this.hitlReviewModal = new HITLReviewModal(page);
- this.requiredActionsButton = page.getByRole("button", { name: "Required
Actions" });
}
public static getDagRunRequiredActionsUrl(dagId: string, dagRunId: string):
string {
@@ -39,10 +37,6 @@ export class DagRunPage extends BasePage {
return `/dags/${dagId}/runs/${dagRunId}`;
}
- public async navigateToDagRun(dagId: string, dagRunId: string):
Promise<void> {
- await this.navigateTo(DagRunPage.getDagRunUrl(dagId, dagRunId));
- }
-
public async navigateToDagRunRequiredActions(dagId: string, dagRunId:
string): Promise<void> {
await this.navigateTo(DagRunPage.getDagRunRequiredActionsUrl(dagId,
dagRunId));
}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsPage.ts
index 4b3310bc6ab..f96f7fe7559 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsPage.ts
@@ -89,38 +89,6 @@ export class DagRunsPage extends BasePage {
/**
* Verify that run details are displayed in the table row
*/
- public async verifyRunDetailsDisplay(): Promise<void> {
- const firstRow = this.dagRunsTable.locator(DATA_ROWS).first();
-
- await expect(firstRow).toBeVisible();
-
- const dagIdLink = firstRow.locator("a[href*='/dags/']").first();
-
- await expect(dagIdLink).toBeVisible();
- await expect(dagIdLink).not.toHaveText("");
-
- const runIdLink = firstRow.locator("a[href*='/runs/']").first();
-
- await expect(runIdLink).toBeVisible();
- await expect(runIdLink).not.toHaveText("");
-
- const stateCell = firstRow.locator("td").filter({ hasText:
/running|success|failed|queued/i });
-
- await expect(stateCell.first()).toBeVisible();
-
- const timeElements = firstRow.locator("time");
-
- if ((await timeElements.count()) > 0) {
- await expect(timeElements.first()).toBeVisible();
- } else {
- const cellTexts = await firstRow.locator("td").allTextContents();
- const hasDateFormat = cellTexts.some((text) =>
-
/\d{4}(?:-\d{2}){2}|(?:\d{1,2}\/){2}\d{4}|(?:\d{1,2}:){2}\d{2}/.test(text),
- );
-
- expect(hasDateFormat).toBeTruthy();
- }
- }
/**
* Verify state filtering via URL parameters
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts
index 3ba98805c2e..71d37bfcd3a 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunsTabPage.ts
@@ -57,18 +57,6 @@ export class DagRunsTabPage extends BasePage {
await this.waitForRunsTableToLoad();
}
- public async filterByState(state: string): Promise<void> {
- const currentUrl = new URL(this.page.url());
-
- currentUrl.searchParams.set("state", state.toLowerCase());
-
- await expect(async () => {
- await this.navigateTo(currentUrl.pathname + currentUrl.search);
- await expect(this.page).toHaveURL(/.*state=.*/);
- await this.waitForRunsTableToLoad();
- }).toPass({ intervals: [2000], timeout: 30_000 });
- }
-
public async navigateToDag(dagId: string): Promise<void> {
await expect(async () => {
await this.navigateTo(`/dags/${dagId}`);
@@ -123,20 +111,6 @@ export class DagRunsTabPage extends BasePage {
return responseJson.dag_run_id;
}
- public async verifyFilteredByState(expectedState: string): Promise<void> {
- await this.waitForRunsTableToLoad();
-
- const rows = this.tableRows;
-
- await expect(rows).not.toHaveCount(0);
-
- const nonMatchingRows = rows.filter({
- hasNot: this.page.getByTestId("state-badge").getByText(new
RegExp(expectedState, "i")),
- });
-
- await expect(nonMatchingRows).toHaveCount(0);
- }
-
public async verifyRunDetailsDisplay(): Promise<void> {
const firstRow = this.tableRows.first();
@@ -156,13 +130,6 @@ export class DagRunsTabPage extends BasePage {
await expect(timeElements.first()).toBeVisible();
}
- public async verifyRunsExist(): Promise<void> {
- const firstRow = this.tableRows.first();
-
- await expect(firstRow).toBeVisible({ timeout: 30_000 });
- await expect(this.tableRows).not.toHaveCount(0);
- }
-
public async verifySearchResults(pattern: string): Promise<void> {
await this.waitForRunsTableToLoad();
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
index 510ab7ac3a6..537e2f88af5 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
@@ -33,20 +33,15 @@ export class DagsPage extends BasePage {
}
public readonly addFilterButton: Locator;
- public readonly cardViewButton: Locator;
public readonly confirmButton: Locator;
public readonly hitlReviewModal: HITLReviewModal;
public readonly needsReviewBadges: Locator;
public readonly needsReviewFilter: Locator;
- public readonly operatorFilter: Locator;
- public readonly retriesFilter: Locator;
public readonly runStateFilter: Locator;
public readonly runStatePill: Locator;
- public readonly searchBox: Locator;
public readonly searchInput: Locator;
public readonly tableViewButton: Locator;
public readonly triggerButton: Locator;
- public readonly triggerRuleFilter: Locator;
public get taskRows(): Locator {
return this.page.locator('[data-testid="table-list"] > tbody > tr');
@@ -58,12 +53,7 @@ export class DagsPage extends BasePage {
// Scoped to the dialog so we never accidentally click the page-level
trigger.
this.confirmButton = page.getByRole("dialog").getByRole("button", { name:
"Trigger" });
- this.searchBox = page.getByRole("textbox", { name: /search/i });
this.searchInput = page.getByPlaceholder("Search Dags");
- this.operatorFilter = page.getByTestId("operator-filter");
- this.triggerRuleFilter = page.getByTestId("trigger-rule-filter");
- this.retriesFilter = page.getByTestId("retries-filter");
- this.cardViewButton = page.getByRole("button", { name: "Show card view" });
this.tableViewButton = page.getByRole("button", { name: "Show table view"
});
// Dags filters live in the shared FilterBar: a filter is added from the
"Add Filter"
// menu, then edited in its pill. Test ids are derived from the
search-param key.
@@ -98,14 +88,6 @@ export class DagsPage extends BasePage {
await this.waitForDagList();
}
- public async filterByOperator(operator: string): Promise<void> {
- await this.selectDropdownOption(this.operatorFilter, operator);
- }
-
- public async filterByRetries(retries: string): Promise<void> {
- await this.selectDropdownOption(this.retriesFilter, retries);
- }
-
/**
* Filter Dags by status
*/
@@ -155,10 +137,6 @@ export class DagsPage extends BasePage {
await responsePromise;
}
- public async filterByTriggerRule(rule: string): Promise<void> {
- await this.selectDropdownOption(this.triggerRuleFilter, rule);
- }
-
/**
* Get a locator for a specific Dag link in the list (card or table view).
*/
@@ -214,14 +192,6 @@ export class DagsPage extends BasePage {
return texts.map((text) => text.trim()).filter((text) => text !== "");
}
- public async getDagNeedsReviewBadgeOnCard(dagId: string): Promise<Locator> {
- const dagCard = this.page.getByTestId("dag-card").filter({ hasText: dagId
});
-
- await expect(dagCard).toBeVisible({ timeout: 60_000 });
-
- return dagCard.getByTestId("needs-review-badge");
- }
-
public async getDagNeedsReviewBadgeOnTable(dagId: string): Promise<Locator> {
const dagRow =
this.page.getByTestId("table-list").getByRole("row").filter({ hasText: dagId });
@@ -244,48 +214,6 @@ export class DagsPage extends BasePage {
: this.page.getByTestId("table-list").locator(DATA_ROWS).count();
}
- public async getFilterOptions(filter: Locator): Promise<Array<string>> {
- const state = await filter.getAttribute("data-state");
-
- if (state === "open") {
- await this.page.keyboard.press("Escape");
- await expect(filter).toHaveAttribute("data-state", "closed", { timeout:
5000 });
- }
-
- await expect(async () => {
- await filter.click({ timeout: 5000 });
- await expect(filter).toHaveAttribute("data-state", "open", { timeout:
5000 });
- }).toPass({ intervals: [1000, 2000], timeout: 15_000 });
-
- const controlsId = await filter.getAttribute("aria-controls");
-
- const dropdown =
- controlsId === null
- ? this.page.locator('div[role="listbox"][data-state="open"]').first()
- : this.page.locator(`[id="${controlsId}"]`);
- const options = dropdown.locator('div[role="option"]');
-
- await expect(options.first()).toBeVisible();
-
- const count = await options.count();
- const dataValues: Array<string> = [];
-
- for (let i = 0; i < count; i++) {
- const value = await options.nth(i).getAttribute("data-value");
-
- if (value !== null && value.trim().length > 0) {
- dataValues.push(value);
- }
- }
-
- // Click outside to dismiss — Escape is unreliable on WebKit.
- await this.page.locator("body").click({ position: { x: 0, y: 0 } });
-
- await
expect(dropdown.and(this.page.locator('[data-state="open"]'))).toBeHidden({
timeout: 5000 });
-
- return dataValues;
- }
-
/**
* Navigate to Dags list page
*/
@@ -379,16 +307,6 @@ export class DagsPage extends BasePage {
await this.waitForDagList();
}
- /**
- * Switch to card view
- */
- public async switchToCardView(): Promise<void> {
- await expect(this.cardViewButton).toBeVisible({ timeout: 30_000 });
- await expect(this.cardViewButton).toBeEnabled();
- await this.cardViewButton.click();
- await this.waitForCardView();
- }
-
/**
* Switch to table view
*/
@@ -412,13 +330,6 @@ export class DagsPage extends BasePage {
return this.handleTriggerDialog();
}
- /**
- * Wait for card view to be visible
- */
- public async waitForCardView(): Promise<void> {
- await expect(this.page.locator('[data-testid="card-list"]')).toBeVisible();
- }
-
/**
* Wait for Dag list to be rendered (card view, table view, or empty state).
*/
@@ -470,30 +381,4 @@ export class DagsPage extends BasePage {
return null;
}
-
- private async selectDropdownOption(filter: Locator, value: string):
Promise<void> {
- await expect(async () => {
- // Dismiss any open dropdown/overlay before clicking the target filter.
- const currentState = await filter.getAttribute("data-state");
-
- if (currentState === "open") {
- await this.page.keyboard.press("Escape");
- await expect(filter).toHaveAttribute("data-state", "closed", {
timeout: 5000 });
- }
-
- await filter.click({ timeout: 5000 });
- await expect(filter).toHaveAttribute("data-state", "open", { timeout:
5000 });
-
- const option =
this.page.locator(`div[role="option"][data-value="${value}"]`);
-
- await expect(option).toBeVisible({ timeout: 5000 });
- await option.click();
-
- if ((await filter.getAttribute("aria-expanded")) === "true") {
- await this.page.keyboard.press("Escape");
- }
-
- await expect(filter).toHaveAttribute("data-state", "closed", { timeout:
5000 });
- }).toPass({ intervals: [1000, 2000], timeout: 30_000 });
- }
}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/XComsPage.ts
b/airflow-core/src/airflow/ui/tests/e2e/pages/XComsPage.ts
index a3ab42c41cf..c4b81f527f2 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/XComsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/XComsPage.ts
@@ -27,16 +27,12 @@ export class XComsPage extends BasePage {
}
public readonly addFilterButton: Locator;
- public readonly collapseAllButton: Locator;
- public readonly expandAllButton: Locator;
public readonly tableRows: Locator;
public readonly xcomsTable: Locator;
public constructor(page: Page) {
super(page);
this.addFilterButton = page.getByTestId("add-filter-button");
- this.collapseAllButton = page.getByTestId("collapse-all-button");
- this.expandAllButton = page.getByTestId("expand-all-button");
this.xcomsTable = page.getByTestId("table-list");
this.tableRows = this.xcomsTable.locator(DATA_ROWS);
}
@@ -98,16 +94,6 @@ export class XComsPage extends BasePage {
}
}
- public async verifyExpandCollapse(): Promise<void> {
- await this.navigate();
-
- await expect(this.expandAllButton.first()).toBeVisible({ timeout: 5000 });
- await this.expandAllButton.first().click();
- await expect(this.collapseAllButton.first()).toBeVisible({ timeout: 15_000
});
-
- await this.collapseAllButton.first().click();
- }
-
public async verifyKeyPatternFiltering(keyPattern: string): Promise<void> {
await this.navigate();
await this.applyFilter("Key", keyPattern);
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts
index af7e3ca13b8..2af18527fe5 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/asset.spec.ts
@@ -27,20 +27,11 @@ test.describe("Assets Page", () => {
await assetListPage.navigate();
});
- test("verify assets page heading", async ({ assetListPage }) => {
- await expect(assetListPage.heading).toBeVisible();
- });
-
- test("verify assets table", async ({ assetListPage }) => {
- await expect(assetListPage.table).toBeVisible();
- });
-
- test("verify asset rows when data exists", async ({ assetListPage }) => {
+ test("verify assets table lists assets with name links", async ({
assetListPage }) => {
+ await expect.soft(assetListPage.heading).toBeVisible();
+ await expect.soft(assetListPage.table).toBeVisible();
await expect(assetListPage.rows.first()).toBeVisible();
- });
-
- test("verify asset has a visible name link", async ({ assetListPage }) => {
- await expect(assetListPage.rows.locator("td a").first()).toBeVisible();
+ await expect.soft(assetListPage.rows.locator("td
a").first()).toBeVisible();
});
test("verify clicking an asset navigates to detail page", async ({
assetListPage, page }) => {
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts
index dd880420fa0..c42e0c47995 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts
@@ -102,70 +102,6 @@ test.describe("Backfill", () => {
expect(details.reprocessBehavior).toEqual(REPROCESS_API_TO_UI[config.behavior]);
});
}
-
- test("Verify backfill table filters", async ({ backfillPage }) => {
- await backfillPage.navigateToBackfillsTab(testDagId);
-
- const tableHeaders = backfillPage.backfillsTable.locator("thead th");
-
- await expect(tableHeaders).toHaveCount(7); // Initial state should have
7 columns
- const initialColumnCount = await tableHeaders.count();
-
- await expect(backfillPage.getFilterButton()).toBeVisible();
-
- await backfillPage.openFilterMenu();
-
- const filterMenuItems = backfillPage.page.getByRole("menuitem");
-
- await expect(filterMenuItems).not.toHaveCount(0);
-
- const firstMenuItem = filterMenuItems.first();
- const columnToToggle = (await firstMenuItem.textContent())?.trim() ?? "";
-
- expect(columnToToggle).not.toBe("");
-
- await backfillPage.toggleColumn(columnToToggle);
- await backfillPage.page.keyboard.press("Escape");
-
- await
expect(backfillPage.getColumnHeader(columnToToggle)).not.toBeVisible();
-
- await expect(tableHeaders).toHaveCount(initialColumnCount - 1);
-
- await backfillPage.openFilterMenu();
- await backfillPage.toggleColumn(columnToToggle);
- await backfillPage.page.keyboard.press("Escape");
-
- await expect(backfillPage.getColumnHeader(columnToToggle)).toBeVisible();
-
- await expect(tableHeaders).toHaveCount(initialColumnCount);
- });
- });
-
- test.describe("validate date range", () => {
- test.setTimeout(30_000);
-
- const testDagId = testConfig.testDag.id;
-
- test("verify date range selection (start date, end date)", async ({
backfillPage }) => {
- await backfillPage.navigateToDagDetail(testDagId);
- await backfillPage.openBackfillDialog();
- // Date-only entry: the control defaults the time (start of day for
From, end of day for To).
- // From (Jan 10) after To (Jan 01) must surface the range error.
- await backfillPage.setBound(backfillPage.backfillFromTrigger,
"2025/01/10");
- await backfillPage.setBound(backfillPage.backfillToTrigger,
"2025/01/01");
- await expect(backfillPage.backfillDateError).toBeVisible();
- });
-
- // Regression for #54429: entering only dates (no time) must yield a valid
range. The range-style
- // text inputs default the missing time, so the form is usable without a
native datetime-local
- // picker (which yielded no value on Firefox/Safari when a date was picked
without a time).
- test("a date-only entry yields a valid range", async ({ backfillPage }) =>
{
- await backfillPage.navigateToDagDetail(testDagId);
- await backfillPage.openBackfillDialog();
- await backfillPage.setBound(backfillPage.backfillFromTrigger,
"2025/01/01");
- await backfillPage.setBound(backfillPage.backfillToTrigger,
"2025/01/05");
- await expect(backfillPage.backfillDateError).not.toBeVisible();
- });
});
test.describe("Backfill pause, resume, and cancel controls", () => {
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-bundles.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-bundles.spec.ts
index f8e51bb215c..51ba7a0c365 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-bundles.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-bundles.spec.ts
@@ -23,10 +23,6 @@ test.describe("Dag Bundles Page", () => {
await dagBundlesPage.navigate();
});
- test("verify dag bundles page heading", async ({ dagBundlesPage }) => {
- await expect(dagBundlesPage.heading).toBeVisible();
- });
-
test("Verify Dag Bundles page is accessible via Browse menu", async ({
dagBundlesPage }) => {
await dagBundlesPage.navigateFromBrowseMenu();
@@ -35,37 +31,24 @@ test.describe("Dag Bundles Page", () => {
expect(await dagBundlesPage.getRowCount()).toBeGreaterThan(0);
});
- test("Verify the dag bundles list displays", async ({ dagBundlesPage }) => {
- await expect(dagBundlesPage.table).toBeVisible();
- });
-
- test("Verify the configured bundle is listed with a name and an active
state", async ({
- dagBundlesPage,
- }) => {
+ test("Verify the configured bundle row describes its state", async ({
dagBundlesPage }) => {
+ await expect.soft(dagBundlesPage.heading).toBeVisible();
+ await expect.soft(dagBundlesPage.table).toBeVisible();
expect(await dagBundlesPage.getRowCount()).toBeGreaterThan(0);
- await expect(dagBundlesPage.nameCellAt(0)).not.toBeEmpty();
+ await expect.soft(dagBundlesPage.nameCellAt(0)).not.toBeEmpty();
// Spelled out both ways rather than left blank when healthy, so the cell
is never empty.
- await
expect(dagBundlesPage.activeCellAt(0)).toHaveText(/^(active|inactive)$/i);
- });
-
- test("Verify the version cell says what it knows", async ({ dagBundlesPage
}) => {
+ await
expect.soft(dagBundlesPage.activeCellAt(0)).toHaveText(/^(active|inactive)$/i);
// The default `dags-folder` bundle does not support versioning, so this
asserts the fallback
// wording rather than a SHA. A versioning bundle would render the short
hexsha instead, and
// "Not refreshed yet" covers one that has never completed a refresh.
- await expect(dagBundlesPage.versionCellAt(0)).toHaveText(
- /^([\da-f]{7}|not versioned|not refreshed yet)$/i,
- );
- });
-
- test("Verify the last refreshed cell is populated", async ({ dagBundlesPage
}) => {
+ await expect
+ .soft(dagBundlesPage.versionCellAt(0))
+ .toHaveText(/^([\da-f]{7}|not versioned|not refreshed yet)$/i);
// Either a relative time or the "Never" fallback, but never blank.
- await expect(dagBundlesPage.lastRefreshedCellAt(0)).not.toBeEmpty();
- });
-
- test("Verify the import errors cell is populated", async ({ dagBundlesPage
}) => {
+ await expect.soft(dagBundlesPage.lastRefreshedCellAt(0)).not.toBeEmpty();
// A number when the caller may read import errors, "-" when they may not.
Admin runs these,
// so a count is expected -- but never a blank cell either way.
- await expect(dagBundlesPage.importErrorsCellAt(0)).toHaveText(/^(\d+|-)$/);
+ await
expect.soft(dagBundlesPage.importErrorsCellAt(0)).toHaveText(/^(\d+|-)$/);
});
});
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 3f17422825e..596951f03ff 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
@@ -29,81 +29,14 @@ test.describe("Dag Calendar Tab", () => {
await dagCalendarTab.navigateToCalendar(calendarRunsData.dagId);
});
- test("verify calendar grid renders", async ({ dagCalendarTab }) => {
+ test("verify success and failed manual runs render as active cells", async
({ dagCalendarTab }) => {
await dagCalendarTab.switchToHourly();
- });
-
- test("verify active cells appear for Dag runs", async ({ dagCalendarTab })
=> {
- await dagCalendarTab.switchToHourly();
-
- const count = await dagCalendarTab.getActiveCellCount();
-
- expect(count).toBeGreaterThan(0);
- });
-
- test("verify manual runs are detected", async ({ dagCalendarTab }) => {
- await dagCalendarTab.switchToHourly();
-
- const states = await dagCalendarTab.getManualRunStates();
- expect(states.length).toBeGreaterThanOrEqual(2);
- });
-
- test("verify success and failed run states are detected", async ({
dagCalendarTab }) => {
- await dagCalendarTab.switchToHourly();
+ expect(await dagCalendarTab.getActiveCellCount()).toBeGreaterThan(0);
const states = await dagCalendarTab.getManualRunStates();
- expect(states).toContain("success");
- expect(states).toContain("failed");
- });
-
- test("failed filter shows only failed runs", async ({ dagCalendarTab }) => {
- await dagCalendarTab.switchToHourly();
-
- const totalStates = await dagCalendarTab.getManualRunStates();
-
- expect(totalStates).toContain("success");
- expect(totalStates).toContain("failed");
-
- await dagCalendarTab.switchToFailedView();
-
- const failedStates = await dagCalendarTab.getManualRunStates();
-
- expect(failedStates).toContain("failed");
- expect(failedStates).not.toContain("success");
- });
-
- test("failed view reduces active cells", async ({ dagCalendarTab }) => {
- await dagCalendarTab.switchToHourly();
-
- const totalCount = await dagCalendarTab.getActiveCellCount();
-
- await dagCalendarTab.switchToFailedView();
-
- const failedCount = await dagCalendarTab.getActiveCellCount();
-
- expect(failedCount).toBeLessThan(totalCount);
- });
-
- test("color scale changes between total and failed view", async ({
dagCalendarTab }) => {
- await dagCalendarTab.switchToHourly();
-
- const totalColors = await dagCalendarTab.getActiveCellColors();
-
- await dagCalendarTab.switchToFailedView();
-
- const failedColors = await dagCalendarTab.getActiveCellColors();
-
- expect(failedColors).not.toEqual(totalColors);
- });
-
- test("cells reflect failed view mode attribute", async ({ dagCalendarTab })
=> {
- await dagCalendarTab.switchToHourly();
- await dagCalendarTab.switchToFailedView();
-
- const cell = dagCalendarTab.activeCells.first();
-
- await expect(cell).toHaveAttribute("data-view-mode", "failed");
+ expect.soft(states).toContain("success");
+ expect.soft(states).toContain("failed");
});
});
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-code-tab.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-code-tab.spec.ts
index 198aadf1155..93342ebf5fa 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-code-tab.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-code-tab.spec.ts
@@ -32,15 +32,7 @@ test.describe("Dag Code Tab", () => {
await dagCodePage.verifySourceCodeDisplayed();
});
- test("Verify syntax highlighting is applied", async ({ dagCodePage }) => {
- await dagCodePage.verifySyntaxHighlighting();
- });
-
test("Verify code is scrollable for long files", async ({ dagCodePage }) => {
await dagCodePage.verifyCodeIsScrollable();
});
-
- test("Verify line numbers are displayed", async ({ dagCodePage }) => {
- await dagCodePage.verifyLineNumbersDisplayed();
- });
});
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-detail.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-detail.spec.ts
deleted file mode 100644
index bf35acc4d0d..00000000000
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-detail.spec.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/*!
- * 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 "tests/e2e/fixtures";
-
-test.describe("Dag Detail Page", () => {
- test("verify HITL review modal opens from Dag detail", async ({
dagDetailPage, pendingHITLRun }) => {
- test.slow();
-
- await dagDetailPage.navigateToDagDetail(pendingHITLRun.dagId);
-
- await expect(dagDetailPage.requiredActionsButton).toBeVisible({ timeout:
60_000 });
- await dagDetailPage.requiredActionsButton.click();
-
- await dagDetailPage.hitlReviewModal.expectOpenWith(pendingHITLRun.dagId);
- });
-
- test("verify HITL review modal opens from the Dag required actions route",
async ({
- dagDetailPage,
- pendingHITLRun,
- }) => {
- test.slow();
-
- await
dagDetailPage.navigateToDagDetailRequiredActions(pendingHITLRun.dagId);
-
- await dagDetailPage.hitlReviewModal.expectOpenWith(pendingHITLRun.dagId);
- });
-});
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-run.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-run.spec.ts
index 704d4682f2d..97e29d285fb 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-run.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-run.spec.ts
@@ -16,20 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { expect, test } from "tests/e2e/fixtures";
+import { test } from "tests/e2e/fixtures";
test.describe("Dag Run Page", () => {
- test("verify HITL review modal opens from Dag run details", async ({
dagRunPage, pendingHITLRun }) => {
- test.slow();
-
- await dagRunPage.navigateToDagRun(pendingHITLRun.dagId,
pendingHITLRun.runId);
-
- await expect(dagRunPage.requiredActionsButton).toBeVisible({ timeout:
60_000 });
- await dagRunPage.requiredActionsButton.click();
-
- await dagRunPage.hitlReviewModal.expectOpenWith(pendingHITLRun.runId);
- });
-
test("verify HITL review modal opens from the required actions route", async
({
dagRunPage,
pendingHITLRun,
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts
index 71618bc168e..7a513e02d6b 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs-tab.spec.ts
@@ -21,45 +21,23 @@ import { expect, test } from "tests/e2e/fixtures";
test.describe("Dag Runs Tab", () => {
test.setTimeout(60_000);
- test("navigate to Dag detail page and click Runs tab", async ({
dagRunsTabPage, successAndFailedRuns }) => {
+ test("navigate to the Runs tab and display run details", async ({
+ dagRunsTabPage,
+ successAndFailedRuns,
+ }) => {
await dagRunsTabPage.navigateToDag(successAndFailedRuns.dagId);
await dagRunsTabPage.clickRunsTab();
await expect(dagRunsTabPage.page).toHaveURL(/.*\/dags\/[^/]+\/runs/);
- });
-
- test("verify run details display correctly", async ({ dagRunsTabPage,
successAndFailedRuns }) => {
- await dagRunsTabPage.navigateToDag(successAndFailedRuns.dagId);
- await dagRunsTabPage.clickRunsTab();
await dagRunsTabPage.verifyRunDetailsDisplay();
});
- test("verify runs exist in table", async ({ dagRunsTabPage,
successAndFailedRuns }) => {
- await dagRunsTabPage.navigateToDag(successAndFailedRuns.dagId);
- await dagRunsTabPage.clickRunsTab();
- await dagRunsTabPage.verifyRunsExist();
- });
-
test("click on a run and verify run details page", async ({ dagRunsTabPage,
successAndFailedRuns }) => {
await dagRunsTabPage.navigateToDag(successAndFailedRuns.dagId);
await dagRunsTabPage.clickRunsTab();
await dagRunsTabPage.clickRunAndVerifyDetails();
});
- test("filter runs by success state", async ({ dagRunsTabPage,
successAndFailedRuns }) => {
- await dagRunsTabPage.navigateToDag(successAndFailedRuns.dagId);
- await dagRunsTabPage.clickRunsTab();
- await dagRunsTabPage.filterByState("success");
- await dagRunsTabPage.verifyFilteredByState("success");
- });
-
- test("filter runs by failed state", async ({ dagRunsTabPage,
successAndFailedRuns }) => {
- await dagRunsTabPage.navigateToDag(successAndFailedRuns.dagId);
- await dagRunsTabPage.clickRunsTab();
- await dagRunsTabPage.filterByState("failed");
- await dagRunsTabPage.verifyFilteredByState("failed");
- });
-
test("search for dag run by run ID pattern", async ({ dagRunsTabPage,
successAndFailedRuns }) => {
await dagRunsTabPage.navigateToDag(successAndFailedRuns.dagId);
await dagRunsTabPage.clickRunsTab();
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts
index 54239f59750..b4b76fce2b9 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts
@@ -30,21 +30,11 @@ test.describe("Dag Runs Page", () => {
await dagRunsPage.verifyDagRunsExist();
});
- test("verify run details display correctly", async ({ dagRunsPage }) => {
- await dagRunsPage.navigate();
- await dagRunsPage.verifyRunDetailsDisplay();
- });
-
test("verify filtering by failed state", async ({ dagRunsPage,
dagRunsPageData }) => {
await dagRunsPage.navigate();
await dagRunsPage.verifyStateFiltering("Failed", dagRunsPageData.dag1Id);
});
- test("verify filtering by success state", async ({ dagRunsPage,
dagRunsPageData }) => {
- await dagRunsPage.navigate();
- await dagRunsPage.verifyStateFiltering("Success", dagRunsPageData.dag1Id);
- });
-
test("verify filtering by Dag ID", async ({ dagRunsPage, dagRunsPageData })
=> {
await dagRunsPage.navigate();
await dagRunsPage.verifyDagIdFiltering(dagRunsPageData.dag1Id);
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-tasks.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-tasks.spec.ts
index d81a7997f51..fa414804993 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-tasks.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-tasks.spec.ts
@@ -32,108 +32,6 @@ test.describe("Dag Tasks Tab", () => {
await expect(firstRow).toContainText("all_success");
});
- test("verify search tasks by name", async ({ dagsPage, successDagRun }) => {
- await dagsPage.navigateToDagTasks(successDagRun.dagId);
-
- const firstTaskLink = dagsPage.taskRows.first().getByRole("link").first();
- const taskName = await firstTaskLink.textContent();
-
- if (taskName === null) {
- throw new Error("Task name not found");
- }
-
- await dagsPage.searchBox.fill(taskName);
-
- await expect.poll(() => dagsPage.taskRows.count(), { timeout: 20_000
}).toBe(1);
- await expect(dagsPage.taskRows).toContainText(taskName);
- });
-
- test("verify filter tasks by operator dropdown", async ({ dagsPage,
successDagRun }) => {
- await dagsPage.navigateToDagTasks(successDagRun.dagId);
-
- let operators: Array<string> = [];
-
- await expect
- .poll(
- async () => {
- operators = await dagsPage.getFilterOptions(dagsPage.operatorFilter);
-
- return operators.length;
- },
- { timeout: 30_000 },
- )
- .toBeGreaterThan(0);
-
- for (const operator of operators) {
- await dagsPage.filterByOperator(operator);
-
- // Use allTextContents() instead of nth(i).textContent() to avoid
- // stale DOM references when React re-renders the table mid-iteration.
- await expect
- .poll(
- async () => {
- const texts = await dagsPage.taskRows.allTextContents();
-
- return texts.length > 0 && texts.every((text) =>
text.includes(operator));
- },
- { timeout: 20_000 },
- )
- .toBeTruthy();
-
- await dagsPage.navigateToDagTasks(successDagRun.dagId);
- }
- });
-
- test("verify filter tasks by trigger rule dropdown", async ({ dagsPage,
successDagRun }) => {
- await dagsPage.navigateToDagTasks(successDagRun.dagId);
-
- let rules: Array<string> = [];
-
- await expect
- .poll(
- async () => {
- rules = await dagsPage.getFilterOptions(dagsPage.triggerRuleFilter);
-
- return rules.length;
- },
- { timeout: 30_000 },
- )
- .toBeGreaterThan(0);
-
- for (const rule of rules) {
- await dagsPage.filterByTriggerRule(rule);
-
- await expect
- .poll(
- async () => {
- const texts = await dagsPage.taskRows.allTextContents();
-
- return texts.length > 0 && texts.every((text) =>
text.includes(rule));
- },
- { timeout: 20_000 },
- )
- .toBeTruthy();
-
- await dagsPage.navigateToDagTasks(successDagRun.dagId);
- }
- });
-
- test("verify filter by retries", async ({ dagsPage, successDagRun }) => {
- await dagsPage.navigateToDagTasks(successDagRun.dagId);
-
- const retriesOptions = await
dagsPage.getFilterOptions(dagsPage.retriesFilter);
-
- if (retriesOptions.length === 0) {
- return;
- }
-
- for (const retries of retriesOptions) {
- await dagsPage.filterByRetries(retries);
- await expect(dagsPage.taskRows.first()).toBeVisible();
- await dagsPage.navigateToDagTasks(successDagRun.dagId);
- }
- });
-
test("verify click task to show details", async ({ dagsPage, page,
successDagRun }) => {
await dagsPage.navigateToDagTasks(successDagRun.dagId);
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
index 5dd3c2c0545..8226903e859 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
@@ -68,17 +68,7 @@ test.describe("Dags List Display", () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function -- triggers
worker-scoped data fixture
test.beforeEach(async ({ dagReady: _ready }) => {});
- test("should display Dags list after successful login", async ({ dagsPage })
=> {
- test.slow();
- await dagsPage.navigate();
- await dagsPage.waitForDagList();
-
- const dagsCount = await dagsPage.getDagsCount();
-
- expect(dagsCount).toBeGreaterThan(0);
- });
-
- test("should display Dag links correctly", async ({ dagsPage }) => {
+ test("should display the Dags list with links to each Dag", async ({
dagsPage }) => {
test.slow();
await dagsPage.navigate();
await dagsPage.waitForDagList();
@@ -88,17 +78,10 @@ test.describe("Dags List Display", () => {
expect(dagLinks.length).toBeGreaterThan(0);
for (const link of dagLinks) {
- expect(link).toMatch(/\/dags\/.+/);
+ expect.soft(link).toMatch(/\/dags\/.+/);
}
- });
-
- test("should display test Dag in the list", async ({ dagsPage }) => {
- test.slow();
- const testDagId = testConfig.testDag.id;
- await dagsPage.navigate();
- await dagsPage.waitForDagList();
- await expect(dagsPage.getDagLink(testDagId)).toBeVisible();
+ await
expect.soft(dagsPage.getDagLink(testConfig.testDag.id)).toBeVisible();
});
test("verify HITL review modal opens from the needs review badge in table
view", async ({
@@ -122,50 +105,6 @@ test.describe("Dags List Display", () => {
await dagsPage.hitlReviewModal.expectOpenWith(pendingHITLRun.dagId);
});
-
- test("verify HITL review modal opens from the needs review badge in card
view", async ({
- dagsPage,
- pendingHITLRun,
- }) => {
- test.slow();
-
- await dagsPage.navigate();
- await dagsPage.waitForDagList();
- await dagsPage.switchToCardView();
-
- await dagsPage.openAddFilterMenu();
- await expect(dagsPage.needsReviewFilter).toBeVisible({ timeout: 30_000 });
- await dagsPage.needsReviewFilter.click();
-
- const needsReviewBadge = await
dagsPage.getDagNeedsReviewBadgeOnCard(pendingHITLRun.dagId);
-
- await expect(needsReviewBadge).toBeVisible({ timeout: 30_000 });
- await needsReviewBadge.click();
-
- await dagsPage.hitlReviewModal.expectOpenWith(pendingHITLRun.dagId);
- });
-});
-
-test.describe("Dags View Toggle", () => {
- test("should toggle between card view and table view", async ({ dagsPage })
=> {
- test.slow();
- await dagsPage.navigate();
- await dagsPage.waitForDagList();
-
- await dagsPage.switchToCardView();
- await dagsPage.waitForCardView();
-
- const cardViewDagsCount = await dagsPage.getDagsCount();
-
- expect(cardViewDagsCount).toBeGreaterThan(0);
-
- await dagsPage.switchToTableView();
- await dagsPage.waitForTableView();
-
- const tableViewDagsCount = await dagsPage.getDagsCount();
-
- expect(tableViewDagsCount).toBeGreaterThan(0);
- });
});
test.describe("Dags Search", () => {
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/plugins.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/plugins.spec.ts
index 7d1face995d..c6a816acb90 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/plugins.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/plugins.spec.ts
@@ -23,40 +23,19 @@ test.describe("Plugins Page", () => {
await pluginsPage.navigate();
});
- test("verify plugins page heading is visible", async ({ pluginsPage }) => {
- await expect(pluginsPage.heading).toBeVisible();
- });
-
- test("verify plugins table is visible", async ({ pluginsPage }) => {
- await expect(pluginsPage.table).toBeVisible();
- });
-
- test("verify plugins list has at least one entry", async ({ pluginsPage })
=> {
+ test("verify plugins list displays each plugin with a name and source",
async ({ pluginsPage }) => {
+ await expect.soft(pluginsPage.heading).toBeVisible();
+ await expect.soft(pluginsPage.table).toBeVisible();
await expect(pluginsPage.rows).not.toHaveCount(0);
- });
- test("verify each plugin has a name", async ({ pluginsPage }) => {
- await expect(pluginsPage.rows).not.toHaveCount(0);
const count = await pluginsPage.rows.count();
- for (let i = 0; i < count; i++) {
- await expect(pluginsPage.nameColumn.nth(i)).not.toBeEmpty();
- }
- });
-
- test("verify each plugin has a source", async ({ pluginsPage }) => {
- await expect(pluginsPage.rows).not.toHaveCount(0);
- const count = await pluginsPage.rows.count();
+ await expect.soft(pluginsPage.nameColumn).toHaveCount(count);
+ await expect.soft(pluginsPage.sourceColumn).toHaveCount(count);
for (let i = 0; i < count; i++) {
- await expect(pluginsPage.sourceColumn.nth(i)).not.toBeEmpty();
+ await expect.soft(pluginsPage.nameColumn.nth(i)).not.toBeEmpty();
+ await expect.soft(pluginsPage.sourceColumn.nth(i)).not.toBeEmpty();
}
});
-
- test("verify plugin names and sources have matching counts", async ({
pluginsPage }) => {
- const rowCount = await pluginsPage.rows.count();
-
- await expect(pluginsPage.nameColumn).toHaveCount(rowCount);
- await expect(pluginsPage.sourceColumn).toHaveCount(rowCount);
- });
});
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts
index c114908f469..340c79842b2 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts
@@ -23,10 +23,6 @@ test.describe("Providers Page", () => {
await providersPage.navigate();
});
- test("verify providers page heading", async ({ providersPage }) => {
- await expect(providersPage.heading).toBeVisible();
- });
-
test("Verify Providers page is accessible via Admin menu", async ({ page,
providersPage }) => {
await page.goto("/");
@@ -42,11 +38,12 @@ test.describe("Providers Page", () => {
expect(await providersPage.getRowCount()).toBeGreaterThan(0);
});
- test("Verify the providers list displays", async ({ providersPage }) => {
- await expect(providersPage.table).toBeVisible();
- });
+ test("Verify the providers list displays package name, version, and
description", async ({
+ providersPage,
+ }) => {
+ await expect.soft(providersPage.heading).toBeVisible();
+ await expect.soft(providersPage.table).toBeVisible();
- test("Verify package name, version, and description are not blank", async ({
providersPage }) => {
const count = await providersPage.getRowCount();
expect(count).toBeGreaterThan(0);
@@ -54,9 +51,9 @@ test.describe("Providers Page", () => {
for (let i = 0; i < 2; i++) {
const { description, packageName, version } = await
providersPage.getRowDetails(i);
- expect(packageName).not.toEqual("");
- expect(version).not.toEqual("");
- expect(description).not.toEqual("");
+ expect.soft(packageName).not.toEqual("");
+ expect.soft(version).not.toEqual("");
+ expect.soft(description).not.toEqual("");
}
});
});
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/task-instances.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/task-instances.spec.ts
index 1fc396737ed..843e204274f 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/task-instances.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/task-instances.spec.ts
@@ -39,9 +39,4 @@ test.describe("Task Instances Page", () => {
await taskInstancesPage.navigate();
await taskInstancesPage.verifyStateFiltering("Failed",
taskInstancesData.dagId);
});
-
- test("verify filtering by success state", async ({ taskInstancesData,
taskInstancesPage }) => {
- await taskInstancesPage.navigate();
- await taskInstancesPage.verifyStateFiltering("Success",
taskInstancesData.dagId);
- });
});
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/task-logs.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/task-logs.spec.ts
index 6c12f45af03..38cb151c03e 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/task-logs.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/task-logs.spec.ts
@@ -21,15 +21,9 @@ import { testConfig } from "playwright.config";
import { expect, test } from "tests/e2e/fixtures";
test.describe("Verify task logs display", () => {
- test.describe.configure({ mode: "serial" });
-
const testTaskId = testConfig.testTask.id;
- test.beforeEach(async ({ executedDagRun, page, taskInstancePage }) => {
- // Clear localStorage — "log settings" test persists toggles that affect
subsequent tests.
- // Swallow SecurityError on about:blank (WebKit).
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- await page.evaluate(() => localStorage.clear()).catch(() => {});
+ test.beforeEach(async ({ executedDagRun, taskInstancePage }) => {
await taskInstancePage.navigateToTaskInstance(executedDagRun.dagId,
executedDagRun.runId, testTaskId);
});
@@ -41,64 +35,12 @@ test.describe("Verify task logs display", () => {
const logItems = page.getByTestId(/^virtualized-item-/);
await expect(logItems.first()).toBeVisible({ timeout: 30_000 });
- });
-
- test("Verify log levels are visible", async ({ executedDagRun: _run, page })
=> {
- const virtualizedList = page.getByTestId("virtualized-list");
-
- await expect(virtualizedList).toBeVisible({ timeout: 30_000 });
-
- await expect(virtualizedList).toContainText(/INFO|WARNING|ERROR|CRITICAL/);
- });
-
- test("Verify log timestamp formatting", async ({ executedDagRun: _run, page
}) => {
- const virtualizedList = page.getByTestId("virtualized-list");
-
- await expect(virtualizedList).toBeVisible({ timeout: 30_000 });
-
- await expect(virtualizedList).toContainText(/\[\d{4}-\d{2}-\d{2}
\d{2}:\d{2}:\d{2}]/, {
- timeout: 30_000,
- });
- });
-
- test("Verify log settings", async ({ executedDagRun: _run, page }) => {
- const virtualizedList = page.getByTestId("virtualized-list");
-
- await expect(virtualizedList).toBeVisible({ timeout: 30_000 });
-
- const logItems = page.getByTestId(/^virtualized-item-/);
-
- await expect(logItems.first()).toBeVisible({ timeout: 30_000 });
- await expect(virtualizedList).toContainText(/\[\d{4}-\d{2}-\d{2}
\d{2}:\d{2}:\d{2}]/, {
+ // Real task output, so changes to the API's log shape or timestamp format
fail here.
+ await expect.soft(virtualizedList).toContainText(/\[\d{4}-\d{2}-\d{2}
\d{2}:\d{2}:\d{2}]/, {
timeout: 30_000,
});
-
- await page.getByTestId("log-settings-button").click();
- await page.getByTestId("log-settings-timestamp").click();
- await expect(virtualizedList).not.toContainText(/\[\d{4}-\d{2}-\d{2}
\d{2}:\d{2}:\d{2}]/);
-
- await expect(virtualizedList).toContainText(/INFO\s-/);
-
- await page.getByTestId("log-settings-button").click();
- await page.getByTestId("log-settings-log-level").click();
- await expect(virtualizedList).not.toContainText(/INFO\s-/);
-
- await page.getByTestId("log-settings-button").click();
- await page.getByTestId("log-settings-source").click();
- await expect(virtualizedList).toContainText(/source/);
-
- await page.getByTestId("log-settings-button").click();
- const wrapMenuItem = page.getByTestId("log-settings-wrap");
-
- await expect(wrapMenuItem).toContainText(/Wrap|Unwrap/);
- await wrapMenuItem.click();
-
- await page.getByTestId("log-settings-button").click();
- const expandMenuItem = page.getByTestId("log-settings-expand");
-
- await expect(expandMenuItem).toContainText(/Expand|Collapse/);
- await expandMenuItem.click();
+ await
expect.soft(virtualizedList).toContainText(/INFO|WARNING|ERROR|CRITICAL/);
});
test("Verify logs are getting downloaded fine", async ({ executedDagRun:
_run, page }) => {
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/xcoms.spec.ts
b/airflow-core/src/airflow/ui/tests/e2e/specs/xcoms.spec.ts
index bf684f3b51f..23548083ea4 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/xcoms.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/xcoms.spec.ts
@@ -26,30 +26,14 @@ test.describe("XComs Page", () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function -- triggers
worker-scoped data fixture
test.beforeEach(async ({ xcomRunsData: _data }) => {});
- test("verify XComs table renders", async ({ xcomsPage }) => {
- await xcomsPage.navigate();
- await expect(xcomsPage.xcomsTable).toBeVisible();
- });
-
- test("verify XComs table displays data", async ({ xcomsPage }) => {
+ test("verify XComs table displays entries with details and values", async ({
xcomsPage }) => {
await xcomsPage.navigate();
+ await expect.soft(xcomsPage.xcomsTable).toBeVisible();
await xcomsPage.verifyXComsExist();
- });
-
- test("verify XCom details display correctly", async ({ xcomsPage }) => {
- await xcomsPage.navigate();
await xcomsPage.verifyXComDetailsDisplay();
- });
-
- test("verify XCom values can be viewed", async ({ xcomsPage }) => {
- await xcomsPage.navigate();
await xcomsPage.verifyXComValuesDisplayed();
});
- test("verify expand/collapse functionality", async ({ xcomsPage }) => {
- await xcomsPage.verifyExpandCollapse();
- });
-
test("verify filtering by key pattern", async ({ xcomRunsData, xcomsPage })
=> {
await xcomsPage.verifyKeyPatternFiltering(xcomRunsData.xcomKey);
});