This is an automated email from the ASF dual-hosted git repository.
potiuk 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 d249f123106 UI: Split log copy and selection tests out of
Logs.test.tsx (#71581)
d249f123106 is described below
commit d249f1231067b120fbc83f9f25f36d1a6eea247e
Author: Stefan Wang <[email protected]>
AuthorDate: Fri Aug 14 06:28:52 2026 -0700
UI: Split log copy and selection tests out of Logs.test.tsx (#71581)
Logs.test.tsx crossed the 500-line max-lines limit, so eslint now fails
static checks on every PR whose merge ref is recomputed against main.
The copy and selection cases are self-contained, so they move to their own
file next to the other per-concern test files in that directory.
Signed-off-by: 1fanwang <[email protected]>
---
.../ui/src/pages/TaskInstance/Logs/Logs.test.tsx | 389 +--------------------
.../Logs/{Logs.test.tsx => LogsSelection.test.tsx} | 389 ---------------------
2 files changed, 1 insertion(+), 777 deletions(-)
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 e0da0bb4bd5..bb3f7953741 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";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
-import { beforeAll, describe, expect, it, vi } from "vitest";
+import { beforeAll, describe, expect, it } from "vitest";
import { AppWrapper } from "src/utils/AppWrapper";
@@ -438,390 +438,3 @@ describe("Task log search", () => {
await expectRenderedLineNumber(/starting attempt 1 of 3/iu, 3);
}, 10_000);
});
-
-const makeClipboardData = () => {
- const store = new Map<string, string>();
-
- return {
- getData: (type: string) => store.get(type) ?? "",
- setData: (type: string, value: string) => store.set(type, value),
- };
-};
-
-const dispatchCopy = (clipboardData: ReturnType<typeof makeClipboardData>) => {
- const copyEvent = new Event("copy", { bubbles: true, cancelable: true });
-
- Object.defineProperty(copyEvent, "clipboardData", { value: clipboardData });
- document.dispatchEvent(copyEvent);
-
- return copyEvent;
-};
-
-const findRow = (text: string) => {
- const container = screen.getByTestId("virtual-scroll-container");
-
- return [...container.querySelectorAll("[data-index]")].find((row) =>
- row.textContent.includes(text),
- ) as HTMLElement;
-};
-
-const getRowCopyText = (row: HTMLElement) => {
- const clone = row.cloneNode(true) as HTMLElement;
-
- for (const element of clone.querySelectorAll("[data-copy-exclude]")) {
- element.remove();
- }
-
- return clone.textContent;
-};
-
-const withFakeSelection = <T,>(selection: Selection, callback: () => T): T => {
- const getSelectionSpy = vi.spyOn(document,
"getSelection").mockReturnValue(selection);
- const result = callback();
-
- getSelectionSpy.mockRestore();
-
- return result;
-};
-
-describe("Copy across virtualized rows", () => {
- it("rebuilds a removed grouped row from log data and strips line numbers",
async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/ti_context"]}
/>,
- );
- await waitForLogs();
-
- fireEvent.click(screen.getByTestId("summary-Pre Execute"));
- await waitFor(() => expect(screen.getByText(/DAG bundles
loaded/iu)).toBeInTheDocument());
-
- const firstRow = findRow("Task started");
- const middleRow = findRow("DAG bundles loaded");
- const lastRow = findRow("Done. Returned value was: None");
-
- expect(firstRow).toBeDefined();
- expect(middleRow).toBeDefined();
- expect(lastRow).toBeDefined();
-
- middleRow.remove();
-
- const range = document.createRange();
-
- range.setStart(firstRow, 0);
- range.setEnd(lastRow, lastRow.childNodes.length);
-
- const selection = { getRangeAt: () => range, isCollapsed: false,
rangeCount: 1 } as unknown as Selection;
- const clipboardData = makeClipboardData();
- const copyEvent = withFakeSelection(selection, () =>
dispatchCopy(clipboardData));
-
- expect(copyEvent.defaultPrevented).toBe(true);
-
- const lines = clipboardData.getData("text/plain").split("\n");
-
- expect(lines[0]).not.toMatch(/^\d/u);
- expect(lines[0]).toContain("Task started");
- expect(lines).toContainEqual(
- expect.stringMatching(/^\[.+\] INFO - DAG bundles loaded: dags-folder,
example_dags$/u),
- );
- });
-
- it("rebuilds a removed ungrouped row from log data", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/ti_context"]}
/>,
- );
- await waitForLogs();
-
- const firstRow = findRow("Log message source details");
- const middleRow = findRow("Task started");
- const lastRow = findRow("Done. Returned value was: None");
-
- expect(firstRow).toBeDefined();
- expect(middleRow).toBeDefined();
- expect(lastRow).toBeDefined();
-
- middleRow.remove();
-
- const range = document.createRange();
-
- range.setStart(firstRow, 0);
- range.setEnd(lastRow, lastRow.childNodes.length);
-
- const selection = { getRangeAt: () => range, isCollapsed: false,
rangeCount: 1 } as unknown as Selection;
- const clipboardData = makeClipboardData();
- const copyEvent = withFakeSelection(selection, () =>
dispatchCopy(clipboardData));
-
- expect(copyEvent.defaultPrevented).toBe(true);
- expect(clipboardData.getData("text/plain").split("\n")).toContainEqual(
- expect.stringMatching(/^\[.+\] INFO - Task started$/u),
- );
- });
-
- it("rebuilds middle rows with the exact text mounted rows show on screen",
async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/ti_context"]}
/>,
- );
- await waitForLogs();
-
- const firstRow = findRow("Log message source details");
- const taskStartedRow = findRow("Task started");
- const headerRow = findRow("Pre Execute");
- const lastRow = findRow("Done. Returned value was: None");
-
- const taskStartedScreenText = getRowCopyText(taskStartedRow);
- const headerScreenText = getRowCopyText(headerRow);
-
- expect(taskStartedScreenText).toMatch(/^\[\d{4}-\d{2}-\d{2}
\d{2}:\d{2}:\d{2}\] INFO - Task started$/u);
- expect(headerScreenText).toBe("▶ Pre Execute");
-
- taskStartedRow.remove();
-
- const range = document.createRange();
-
- range.setStart(firstRow, 0);
- range.setEnd(lastRow, lastRow.childNodes.length);
-
- const selection = { getRangeAt: () => range, isCollapsed: false,
rangeCount: 1 } as unknown as Selection;
- const clipboardData = makeClipboardData();
-
- withFakeSelection(selection, () => dispatchCopy(clipboardData));
-
- const lines = clipboardData.getData("text/plain").split("\n");
-
- expect(lines[0]).toBe("▶ Log message source details");
- expect(lines).toContain(taskStartedScreenText);
- expect(lines).toContain(headerScreenText);
- });
-
- it("copies the expanded marker for expanded group headers", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/ti_context"]}
/>,
- );
- await waitForLogs();
-
- fireEvent.click(screen.getByTestId("summary-Pre Execute"));
- await waitFor(() => expect(getRowCopyText(findRow("Pre Execute"))).toBe("▼
Pre Execute"));
-
- const firstRow = findRow("Log message source details");
- const headerRow = findRow("Pre Execute");
- const taskStartedRow = findRow("Task started");
- const lastRow = findRow("Done. Returned value was: None");
-
- taskStartedRow.remove();
- headerRow.remove();
-
- const range = document.createRange();
-
- range.setStart(firstRow, 0);
- range.setEnd(lastRow, lastRow.childNodes.length);
-
- const selection = { getRangeAt: () => range, isCollapsed: false,
rangeCount: 1 } as unknown as Selection;
- const clipboardData = makeClipboardData();
-
- withFakeSelection(selection, () => dispatchCopy(clipboardData));
-
- expect(clipboardData.getData("text/plain").split("\n")).toContain("▼ Pre
Execute");
- });
-
- it("leaves single-row selections to native copy", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/ti_context"]}
/>,
- );
- await waitForLogs();
-
- const row = findRow("Task started");
-
- expect(row).toBeDefined();
-
- const range = document.createRange();
-
- range.setStart(row, 0);
- range.setEnd(row, row.childNodes.length);
-
- const selection = { getRangeAt: () => range, isCollapsed: false,
rangeCount: 1 } as unknown as Selection;
- const clipboardData = makeClipboardData();
- const copyEvent = withFakeSelection(selection, () =>
dispatchCopy(clipboardData));
-
- expect(copyEvent.defaultPrevented).toBe(false);
- expect(clipboardData.getData("text/plain")).toBe("");
- });
-});
-
-describe("Selection pinning across scrolling", () => {
- it("keeps the selection-anchor row mounted after scrolling it out of the
render window", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/generate"]}
/>,
- );
- await waitForLogs();
-
- fireEvent.click(screen.getByTestId("summary-Pre task execution logs"));
- await waitFor(() => expect(screen.getByText(/starting attempt 1 of
3/iu)).toBeInTheDocument());
-
- const anchorRow = findRow("Starting attempt 1 of 3");
- const anchorIndex = Number(anchorRow.getAttribute("data-index"));
- const neighborIndex = anchorIndex + 1;
- const textNode = anchorRow.querySelector("span")?.firstChild as Node;
- const range = document.createRange();
-
- range.setStart(textNode, 0);
- range.setEnd(textNode, 0);
-
- const selection = { getRangeAt: () => range, isCollapsed: true,
rangeCount: 1 } as unknown as Selection;
-
- withFakeSelection(selection, () => {
- document.dispatchEvent(new Event("selectionchange"));
- });
-
- const container = screen.getByTestId("virtual-scroll-container");
-
- fireEvent.scroll(container, { target: { scrollTop: ITEM_HEIGHT *
(anchorIndex + 15) } });
-
- await waitFor(() => {
-
expect(container.querySelector(`[data-index="${neighborIndex}"]`)).toBeNull();
- });
-
expect(container.querySelector(`[data-index="${anchorIndex}"]`)).not.toBeNull();
- });
-
- it("unpins once the selection is cleared", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/generate"]}
/>,
- );
- await waitForLogs();
-
- fireEvent.click(screen.getByTestId("summary-Pre task execution logs"));
- await waitFor(() => expect(screen.getByText(/starting attempt 1 of
3/iu)).toBeInTheDocument());
-
- const anchorRow = findRow("Starting attempt 1 of 3");
- const anchorIndex = Number(anchorRow.getAttribute("data-index"));
- const textNode = anchorRow.querySelector("span")?.firstChild as Node;
- const range = document.createRange();
-
- range.setStart(textNode, 0);
- range.setEnd(textNode, 0);
-
- const selection = { getRangeAt: () => range, isCollapsed: true,
rangeCount: 1 } as unknown as Selection;
-
- withFakeSelection(selection, () => {
- document.dispatchEvent(new Event("selectionchange"));
- });
-
- const noSelection = null as unknown as Selection;
-
- withFakeSelection(noSelection, () => {
- document.dispatchEvent(new Event("selectionchange"));
- });
-
- const container = screen.getByTestId("virtual-scroll-container");
-
- fireEvent.scroll(container, { target: { scrollTop: ITEM_HEIGHT *
(anchorIndex + 15) } });
-
- await waitFor(() => {
-
expect(container.querySelector(`[data-index="${anchorIndex}"]`)).toBeNull();
- });
- });
-});
-
-describe("Downward drag selection", () => {
- it("coalesces events and extends the selection to the mounted bottom row",
async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/ti_context"]}
/>,
- );
- await waitForLogs();
-
- const container = screen.getByTestId("virtual-scroll-container");
- const rows = container.querySelectorAll<HTMLElement>("[data-index]");
- const anchorRow = rows[0] as HTMLElement;
- const lastRow = rows[rows.length - 1] as HTMLElement;
- const extend = vi.fn();
- const range = document.createRange();
-
- range.selectNodeContents(anchorRow);
-
- const selection = {
- anchorNode: anchorRow,
- extend,
- focusNode: anchorRow,
- focusOffset: 0,
- getRangeAt: () => range,
- rangeCount: 1,
- } as unknown as Selection;
- const animationFrames = new Array<FrameRequestCallback>();
- const getSelectionSpy = vi.spyOn(document,
"getSelection").mockReturnValue(selection);
- const requestAnimationFrameSpy = vi
- .spyOn(globalThis, "requestAnimationFrame")
- .mockImplementation((callback) => {
- animationFrames.push(callback);
-
- return animationFrames.length;
- });
- const cancelAnimationFrameSpy = vi
- .spyOn(globalThis, "cancelAnimationFrame")
- .mockImplementation(() => undefined);
-
- container.getBoundingClientRect = () => ({ bottom: 500 }) as DOMRect;
- lastRow.getBoundingClientRect = () => ({ bottom: 480 }) as DOMRect;
-
- fireEvent.pointerDown(anchorRow, { button: 0, clientY: 200, pointerType:
"mouse" });
- fireEvent.pointerMove(document, { clientY: 490, pointerType: "mouse" });
- document.dispatchEvent(new Event("selectionchange"));
- fireEvent.scroll(container);
-
- expect(animationFrames).toHaveLength(1);
- animationFrames.shift()?.(0);
- expect(extend).toHaveBeenCalledWith(lastRow, lastRow.childNodes.length);
-
- fireEvent.scroll(container);
- expect(animationFrames).toHaveLength(1);
- animationFrames.shift()?.(1);
- expect(extend).toHaveBeenCalledTimes(2);
-
- fireEvent.scroll(container);
- const pendingAnimationFrame = animationFrames.shift();
-
- fireEvent.pointerUp(document, { pointerType: "mouse" });
- expect(cancelAnimationFrameSpy).toHaveBeenCalledWith(1);
-
- pendingAnimationFrame?.(2);
- expect(extend).toHaveBeenCalledTimes(2);
-
- getSelectionSpy.mockRestore();
- requestAnimationFrameSpy.mockRestore();
- cancelAnimationFrameSpy.mockRestore();
- });
-
- it("only activates for downward primary-mouse drags starting in a log row",
async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/ti_context"]}
/>,
- );
- await waitForLogs();
-
- const container = screen.getByTestId("virtual-scroll-container");
- const anchorRow = container.querySelector<HTMLElement>("[data-index]") as
HTMLElement;
- const rows = container.querySelectorAll<HTMLElement>("[data-index]");
- const lastRow = rows[rows.length - 1] as HTMLElement;
- const requestAnimationFrameSpy = vi
- .spyOn(globalThis, "requestAnimationFrame")
- .mockImplementation(() => 1);
-
- container.getBoundingClientRect = () => ({ bottom: 500 }) as DOMRect;
- lastRow.getBoundingClientRect = () => ({ bottom: 700 }) as DOMRect;
-
- fireEvent.pointerDown(container, { button: 0, clientY: 200, pointerType:
"mouse" });
- fireEvent.pointerMove(document, { clientY: 600, pointerType: "mouse" });
- expect(requestAnimationFrameSpy).not.toHaveBeenCalled();
-
- fireEvent.pointerDown(anchorRow, { button: 2, clientY: 200, pointerType:
"mouse" });
- fireEvent.pointerMove(document, { clientY: 600, pointerType: "mouse" });
- expect(requestAnimationFrameSpy).not.toHaveBeenCalled();
-
- fireEvent.pointerDown(anchorRow, { button: 0, clientY: 200, pointerType:
"touch" });
- fireEvent.pointerMove(document, { clientY: 600, pointerType: "touch" });
- expect(requestAnimationFrameSpy).not.toHaveBeenCalled();
-
- fireEvent.pointerDown(anchorRow, { button: 0, clientY: 200, pointerType:
"mouse" });
- fireEvent.pointerMove(document, { clientY: 50, pointerType: "mouse" });
- fireEvent.scroll(container);
- expect(requestAnimationFrameSpy).not.toHaveBeenCalled();
-
- fireEvent.pointerUp(document, { pointerType: "mouse" });
- requestAnimationFrameSpy.mockRestore();
- });
-});
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/LogsSelection.test.tsx
similarity index 52%
copy from airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx
copy to
airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/LogsSelection.test.tsx
index e0da0bb4bd5..b48c89f5572 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx
+++
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/LogsSelection.test.tsx
@@ -50,395 +50,6 @@ const waitForLogs = async () => {
fireEvent.scroll(screen.getByTestId("virtualized-list"), { target: {
scrollTop: ITEM_HEIGHT * 2 } });
};
-describe("Task log source", () => {
- it("Toggles logger and location on click", async () => {
- render(
- // <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-09-11T17:44:49.064088+00:00/tasks/source_testing"]}
/>,
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/log_source"]}
/>,
- );
-
- await waitForLogs();
-
- // Source should be hidden by default
- expect(document.querySelector('[data-key="logger"]')).toBeNull();
- expect(document.querySelector('[data-key="loc"]')).toBeNull();
-
- // Toggle source on
- fireEvent.keyDown(document.activeElement ?? document.body, { code: "KeyS",
key: "S" });
- fireEvent.keyPress(document.activeElement ?? document.body, { code:
"KeyS", key: "S" });
- fireEvent.keyUp(document.activeElement ?? document.body, { code: "KeyS",
key: "S" });
-
- const dagBagRow = (await screen.findByText(/Filling up the
DagBag/iu)).closest(
- '[data-testid^="virtualized-item-"]',
- );
- const source = dagBagRow?.querySelector('[data-key="logger"]') ??
undefined;
- const loc = dagBagRow?.querySelector('[data-key="loc"]') ?? undefined;
-
- // Source should now be visible
- expect(source).toBeVisible();
- expect(source).toHaveProperty("innerText",
"source=airflow.models.dagbag.DagBag");
-
- expect(loc).toBeVisible();
- expect(loc).toHaveProperty("innerText", "loc=dagbag.py:593");
- });
-});
-describe("Task log grouping", () => {
- it("Display task log content on click", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/generate"]}
/>,
- );
-
- await waitForLogs();
-
- // Group headers use the summary-{name} testid pattern and are always
visible
- const summarySource = screen.getByTestId("summary-Log message source
details");
-
- expect(summarySource).toBeVisible();
-
- const summaryPre = screen.getByTestId("summary-Pre task execution logs");
-
- expect(summaryPre).toBeVisible();
-
- // All groups start collapsed. Verify Post header is in the virtualizer
range.
- const summaryPost = screen.getByTestId("summary-Post task execution logs");
-
- expect(summaryPost).toBeVisible();
-
- // Groups start collapsed — content should not be in DOM
- expect(screen.queryByText(/starting attempt 1 of 3/iu)).toBeNull();
-
- // Click to expand Pre
- fireEvent.click(summaryPre);
- await waitFor(() => expect(screen.getByText(/starting attempt 1 of
3/iu)).toBeInTheDocument());
-
- // Click Pre to collapse
- fireEvent.click(summaryPre);
- await waitFor(() => expect(screen.queryByText(/Task instance is in running
state/iu)).toBeNull());
-
- // Click Pre to expand again
- fireEvent.click(summaryPre);
- await waitFor(() =>
- expect(screen.queryByText(/Task instance is in running
state/iu)).toBeInTheDocument(),
- );
-
- // Click Pre to collapse again (to return to compact view)
- fireEvent.click(summaryPre);
- await waitFor(() => expect(screen.queryByText(/Task instance is in running
state/iu)).toBeNull());
-
- // Now expand Post (which is visible because Pre is collapsed again)
- fireEvent.click(screen.getByTestId("summary-Post task execution logs"));
- await waitFor(() => expect(screen.queryByText(/Marking task as
SUCCESS/iu)).toBeInTheDocument());
-
- // Collapse Post
- fireEvent.click(screen.getByTestId("summary-Post task execution logs"));
- await waitFor(() => expect(screen.queryByText(/Marking task as
SUCCESS/iu)).toBeNull());
-
- // Test Expand All / Collapse All via settings menu
- const settingsBtn = screen.getByTestId("log-settings-button");
-
- fireEvent.click(settingsBtn);
-
- const expandItem = await screen.findByRole("menuitem", { name: /expand/iu
});
-
- fireEvent.click(expandItem);
-
- // After "Expand All", Pre group content near the top should be visible
- await waitFor(() => expect(screen.queryByText(/starting attempt 1 of
3/iu)).toBeInTheDocument());
-
- /* ─── Click "Collapse" ─── */
- fireEvent.click(settingsBtn);
- const collapseItem = await screen.findByRole("menuitem", { name:
/collapse/iu });
-
- fireEvent.click(collapseItem);
-
- // After "Collapse All", group content should be gone from the DOM
- await waitFor(() => expect(screen.queryByText(/starting attempt 1 of
3/iu)).toBeNull());
- }, 10_000);
-
- it("renders nested groups correctly", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/generate"]}
/>,
- );
-
- await waitForLogs();
-
- // The nested group "Dependency check details" should have a header
- // First expand the parent group "Pre task execution logs"
- const summaryPre = screen.getByTestId("summary-Pre task execution logs");
-
- fireEvent.click(summaryPre);
-
- // The nested group header should now be visible
- await waitFor(() => expect(screen.getByTestId("summary-Dependency check
details")).toBeInTheDocument());
-
- // But nested group content is collapsed
- expect(screen.queryByText(/dep_context=non-requeueable/iu)).toBeNull();
-
- // Expand the nested group
- fireEvent.click(screen.getByTestId("summary-Dependency check details"));
- await waitFor(() =>
expect(screen.getByText(/dep_context=non-requeueable/iu)).toBeInTheDocument());
- }, 10_000);
-});
-
-describe("Task Identity preamble", () => {
- it("renders Task Identity preamble after the 'Pre Execute' group header as
first group element", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/ti_context"]}
/>,
- );
-
- await waitForLogs();
-
- const sourceGroup = screen.getByTestId("summary-Log message source
details");
-
- expect(sourceGroup).toBeInTheDocument();
-
- // Expand the Pre Execute group to reveal the preamble
- const groupHeader = screen.getByTestId("summary-Pre Execute");
-
- fireEvent.click(groupHeader);
-
- // Task Identity preamble should be visible after expanding the group
- await waitFor(() => expect(screen.getByText("Task
Identity")).toBeInTheDocument());
- expect(screen.getByText("ti_id")).toBeInTheDocument();
- // Value is a text node adjacent to =; match via partial text
-
expect(screen.getByText(/01951900-16f6-7c1c-ae66-91bdfe9e0cfd/u)).toBeInTheDocument();
- expect(screen.getByText("Done. Returned value was:
None")).toBeInTheDocument();
-
- // Preamble should come after the "Pre Execute" group header in DOM order.
- const preamble = screen.getByText("Task Identity");
-
- expect(preamble).toBeInTheDocument();
- expect(groupHeader).toBeInTheDocument();
-
- // DOCUMENT_POSITION_FOLLOWING (4) is set when preamble comes after
groupHeader
- // eslint-disable-next-line no-bitwise
- expect(groupHeader.compareDocumentPosition(preamble) &
Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
- });
-
- it("does not render TI context fields on individual log lines", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/ti_context"]}
/>,
- );
-
- await waitForLogs();
-
- const taskStarted = screen.getByText("Task
started").closest('[data-testid^="virtualized-item-"]');
-
- expect(taskStarted).toBeInTheDocument();
-
- if (taskStarted !== null) {
- expect(taskStarted.querySelector('[data-key="ti_id"]')).toBeNull();
- expect(taskStarted.querySelector('[data-key="dag_id"]')).toBeNull();
- expect(taskStarted.querySelector('[data-key="run_id"]')).toBeNull();
- }
- });
-});
-
-describe("Task log search", () => {
- it("search input is rendered in the log header", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/log_source"]}
/>,
- );
-
- await waitForLogs();
-
- expect(screen.getByTestId("log-search-input")).toBeInTheDocument();
- });
-
- it("typing in the search input enables navigation buttons for a known term",
async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/log_source"]}
/>,
- );
-
- await waitForLogs();
-
- const searchInput = screen.getByTestId("log-search-input");
-
- // "running state" appears in the mock log data
- fireEvent.change(searchInput, { target: { value: "running state" } });
-
- // Navigation buttons should become enabled once matches are found
- await waitFor(() => {
- expect(screen.getByRole("button", { name: /next match/iu
})).not.toBeDisabled();
- expect(screen.getByRole("button", { name: /previous match/iu
})).not.toBeDisabled();
- });
- });
-
- it("shows no-matches indicator for a term that does not exist in logs",
async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/log_source"]}
/>,
- );
-
- await waitForLogs();
-
- const searchInput = screen.getByTestId("log-search-input");
-
- fireEvent.change(searchInput, { target: { value: "zzz_not_in_logs_zzz" }
});
-
- await waitFor(() => {
- // Navigation buttons should be disabled with zero matches
- const nextBtn = screen.getByRole("button", { name: /next match/iu });
-
- expect(nextBtn).toBeDisabled();
- });
- });
-
- it("pressing Escape clears the search query", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/log_source"]}
/>,
- );
-
- await waitForLogs();
-
- const searchInput = screen.getByTestId("log-search-input");
-
- fireEvent.change(searchInput, { target: { value: "running" } });
-
- await waitFor(() => expect(screen.queryByRole("button", { name: /next
match/iu })).toBeInTheDocument());
-
- fireEvent.keyDown(searchInput, { key: "Escape" });
-
- await waitFor(() => {
- expect((searchInput as HTMLInputElement).value).toBe("");
- });
- });
-
- it("pressing Enter keeps navigation buttons enabled (navigates to next
match)", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/log_source"]}
/>,
- );
-
- await waitForLogs();
-
- const searchInput = screen.getByTestId("log-search-input");
-
- // "state" appears multiple times in the mock log data
- fireEvent.change(searchInput, { target: { value: "state" } });
-
- // Wait for matches to be found (navigation buttons become enabled)
- await waitFor(() => expect(screen.getByRole("button", { name: /next
match/iu })).not.toBeDisabled());
-
- // Navigate forward — buttons remain enabled
- fireEvent.keyDown(searchInput, { key: "Enter" });
-
- await waitFor(() => {
- expect(screen.getByRole("button", { name: /next match/iu
})).not.toBeDisabled();
- });
- });
-
- it("pressing Shift+Enter keeps navigation buttons enabled (navigates to
previous match)", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/log_source"]}
/>,
- );
-
- await waitForLogs();
-
- const searchInput = screen.getByTestId("log-search-input");
-
- fireEvent.change(searchInput, { target: { value: "state" } });
-
- // Wait for matches to be found
- await waitFor(() => expect(screen.getByRole("button", { name: /previous
match/iu })).not.toBeDisabled());
-
- // Navigate backward — buttons remain enabled
- fireEvent.keyDown(searchInput, { key: "Enter", shiftKey: true });
-
- await waitFor(() => {
- expect(screen.getByRole("button", { name: /previous match/iu
})).not.toBeDisabled();
- });
- });
-
- it("search finds matches per-line inside log groups (not collapsed into 1
match per group)", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/generate"]}
/>,
- );
-
- await waitForLogs();
-
- const searchInput = screen.getByTestId("log-search-input");
-
- // "INFO" appears in many individual log lines across multiple groups.
- // With the old code, each group collapsed into 1 match. Now each line is
a separate match.
- fireEvent.change(searchInput, { target: { value: "INFO" } });
-
- await waitFor(() => {
- expect(screen.getByRole("button", { name: /next match/iu
})).not.toBeDisabled();
- });
-
- // Verify per-line search by navigating through matches.
- // With the old code (1 match per group), there were only 3 matches for
"INFO".
- // Now each line is a separate match, so we can navigate through many more.
- const nextBtn = screen.getByRole("button", { name: /next match/iu });
-
- // Navigate forward 4 times — if only 3 matches existed, the 4th click
would wrap to #1
- // We just verify it stays enabled (which it always does with >0 matches).
- // The real proof is that the auto-expand test passes: searching "starting
attempt"
- // finds a match INSIDE a group, which was impossible with the old
collapsed approach.
- for (let navStep = 0; navStep < 4; navStep += 1) {
- fireEvent.click(nextBtn);
- }
-
- await waitFor(() => {
- expect(nextBtn).not.toBeDisabled();
- });
- }, 10_000);
-
- it("search navigating to match in collapsed group auto-expands it", async ()
=> {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/generate"]}
/>,
- );
-
- await waitForLogs();
-
- // All groups start collapsed. Search for text that only appears inside a
group.
- const searchInput = screen.getByTestId("log-search-input");
-
- fireEvent.change(searchInput, { target: { value: "starting attempt" } });
-
- await waitFor(() => {
- const nextBtn = screen.getByRole("button", { name: /next match/iu });
-
- expect(nextBtn).not.toBeDisabled();
- });
-
- // The match is inside "Pre task execution logs" group.
- // Auto-expand should make it visible.
- await waitFor(() => expect(screen.getByText(/starting attempt 1 of
3/iu)).toBeInTheDocument());
- }, 10_000);
-
- it("skips group markers when assigning line numbers", async () => {
- render(
- <AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/generate"]}
/>,
- );
-
- await waitForLogs();
-
- const expectRenderedLineNumber = async (pattern: RegExp,
expectedLineNumber: number) => {
- const row = (await
screen.findByText(pattern)).closest('[data-testid^="virtualized-item-"]');
-
- expect(row).not.toBeNull();
-
- const anchor = row?.querySelector<HTMLAnchorElement>("a[id]");
-
- expect(anchor).not.toBeNull();
-
- expect(Number(anchor?.id)).toBe(expectedLineNumber);
- };
-
- const summaryPre = screen.getByTestId("summary-Pre task execution logs");
-
- fireEvent.click(summaryPre);
-
- const summaryDependency = await screen.findByTestId("summary-Dependency
check details");
-
- fireEvent.click(summaryDependency);
-
- await expectRenderedLineNumber(/dep_context=non-requeueable/iu, 1);
- await expectRenderedLineNumber(/dep_context=requeueable/iu, 2);
- await expectRenderedLineNumber(/starting attempt 1 of 3/iu, 3);
- }, 10_000);
-});
-
const makeClipboardData = () => {
const store = new Map<string, string>();