FN-7375: fix Activity dropdown clipping
Keep the task Activity view selector visible without blanking the mobile tab strip. - Portal the Activity view menu to the document body and clamp its fixed position to the visual viewport. - Close and refocus the menu safely across task changes, scrolling, resizing, keyboard selection, and outside clicks. - Add regression coverage for mobile clipping, tab-strip preservation, and the changeset release note. Files changed: .changeset/fn-7375-activity-dropdown-overlay.md | 7 + .../dashboard/app/components/TaskDetailModal.css | 15 +- .../dashboard/app/components/TaskDetailModal.tsx | 186 ++++++++++++++++++--- .../__tests__/TaskDetailModal.css.test.ts | 7 +- ...etailModal.responsive-and-dependencies.test.tsx | 12 +- .../TaskDetailModal.task-activity-chat.test.tsx | 61 +++++++ 6 files changed, 254 insertions(+), 34 deletions(-) Fusion-Task-Id: FN-7375 Fusion-Task-Lineage: 5999507a-edd3-4485-935a-885f7c143ed4 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7375-activity-dropdown-overlay.md
Normal file
7
.changeset/fn-7375-activity-dropdown-overlay.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix the mobile task Activity view dropdown so it opens above the tab strip without clipping.
|
||||
category: fix
|
||||
dev: Root-portals and viewport-clamps the task-detail Activity Live/Feed/Raw menu with regression coverage.
|
||||
@@ -2503,6 +2503,9 @@ FNXC:TaskDetailTabs 2026-06-26-00:35:
|
||||
/*
|
||||
FNXC:TaskDetailActivity 2026-06-30-23:59:
|
||||
The Activity tab itself is the Live/Feed/Raw dropdown trigger. Keep it in the `.detail-tabs` scroller with the same tab sizing and active underline, and keep the Activity panel toolbar only for the expand affordance so no duplicate in-content selector or empty mobile shell remains.
|
||||
|
||||
FNXC:TaskDetailActivity 2026-07-01-00:00:
|
||||
The Activity view menu is portaled to the document body and fixed-positioned because `.detail-tabs` uses overflow scrolling on mobile. Keep overflow containment on the tab strip and move only the menu overlay out of that clipping chain.
|
||||
*/
|
||||
.detail-tab-dropdown {
|
||||
position: relative;
|
||||
@@ -2522,12 +2525,8 @@ The Activity tab itself is the Live/Feed/Raw dropdown trigger. Keep it in the `.
|
||||
}
|
||||
|
||||
.activity-view-menu {
|
||||
position: absolute;
|
||||
inset-block-start: 100%;
|
||||
inset-inline-start: 0;
|
||||
z-index: 20;
|
||||
min-inline-size: 100%;
|
||||
margin-block-start: var(--space-xs);
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
padding: var(--space-xs);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2536,6 +2535,8 @@ The Activity tab itself is the Live/Feed/Raw dropdown trigger. Keep it in the `.
|
||||
border: var(--btn-border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.activity-view-menu-item {
|
||||
@@ -2596,7 +2597,7 @@ The Activity tab itself is the Live/Feed/Raw dropdown trigger. Keep it in the `.
|
||||
}
|
||||
|
||||
.activity-view-menu {
|
||||
min-inline-size: calc(100% + var(--space-xl));
|
||||
max-inline-size: calc(100vw - (var(--space-md) * 2));
|
||||
}
|
||||
|
||||
.activity-expand-toggle {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "./TaskDetailModal.css";
|
||||
import React, { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles, Maximize2, Minimize2 } from "lucide-react";
|
||||
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||
@@ -73,6 +74,18 @@ import type { DetailTaskInitialActionRequest } from "../hooks/useModalManager";
|
||||
const STALE_PAUSED_REVIEW_LOG_REGEX = /^Stale paused review surfaced \[([^\]]+)\]/;
|
||||
const EMPTY_MARKDOWN_CHILD_SEPARATOR = "";
|
||||
const STRING_OBJECT_TAG = "[object String]";
|
||||
const ACTIVITY_VIEW_MENU_VIEWPORT_PADDING = 16;
|
||||
const ACTIVITY_VIEW_MENU_TRIGGER_GAP = 4;
|
||||
const ACTIVITY_VIEW_MENU_MIN_WIDTH = 160;
|
||||
const ACTIVITY_VIEW_MENU_MIN_HEIGHT = 120;
|
||||
const ACTIVITY_VIEW_MENU_MAX_HEIGHT = 320;
|
||||
|
||||
type ActivityViewMenuPosition = {
|
||||
top: number;
|
||||
left: number;
|
||||
minWidth: number;
|
||||
maxHeight: number;
|
||||
};
|
||||
|
||||
function isStringValue(value: unknown): value is string {
|
||||
return Object.prototype.toString.call(value) === STRING_OBJECT_TAG;
|
||||
@@ -922,6 +935,7 @@ export function TaskDetailContent({
|
||||
const [showMoveMenu, setShowMoveMenu] = useState(false);
|
||||
const [showActionsMenu, setShowActionsMenu] = useState(false);
|
||||
const [showActivityViewMenu, setShowActivityViewMenu] = useState(false);
|
||||
const [activityViewMenuPosition, setActivityViewMenuPosition] = useState<ActivityViewMenuPosition | null>(null);
|
||||
const [sourceIssueExpanded, setSourceIssueExpanded] = useState(false);
|
||||
const [retriesExpanded, setRetriesExpanded] = useState(initialTab === "retries");
|
||||
const [githubTrackingExpanded, setGithubTrackingExpanded] = useState(false);
|
||||
@@ -934,6 +948,7 @@ export function TaskDetailContent({
|
||||
const activityListRef = useRef<HTMLDivElement>(null);
|
||||
const moveButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const actionsMenuRef = useRef<HTMLDivElement>(null);
|
||||
const activityViewDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const activityViewMenuRef = useRef<HTMLDivElement>(null);
|
||||
const activityViewButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -1278,7 +1293,7 @@ export function TaskDetailContent({
|
||||
const target = e.target as Node;
|
||||
const inMoveMenu = moveMenuRef.current?.contains(target);
|
||||
const inActionsMenu = actionsMenuRef.current?.contains(target);
|
||||
const inActivityViewMenu = activityViewMenuRef.current?.contains(target);
|
||||
const inActivityViewMenu = activityViewMenuRef.current?.contains(target) || activityViewButtonRef.current?.contains(target);
|
||||
|
||||
if (!inMoveMenu && showMoveMenu) {
|
||||
setShowMoveMenu(false);
|
||||
@@ -2819,6 +2834,68 @@ export function TaskDetailContent({
|
||||
closeMoveMenuAndFocusTrigger();
|
||||
}, [closeMoveMenuAndFocusTrigger]);
|
||||
|
||||
const closeActivityViewMenuAndFocusTrigger = useCallback(() => {
|
||||
setShowActivityViewMenu(false);
|
||||
setActivityViewMenuPosition(null);
|
||||
activityViewButtonRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const getEffectiveViewport = useCallback(() => {
|
||||
const visualViewport = window.visualViewport;
|
||||
if (visualViewport && visualViewport.width > 0 && visualViewport.height > 0) {
|
||||
return {
|
||||
width: visualViewport.width,
|
||||
height: visualViewport.height,
|
||||
offsetTop: visualViewport.offsetTop,
|
||||
offsetLeft: visualViewport.offsetLeft,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
offsetTop: 0,
|
||||
offsetLeft: 0,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateActivityViewMenuPosition = useCallback(() => {
|
||||
const trigger = activityViewButtonRef.current;
|
||||
if (!trigger) return;
|
||||
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
const { width: viewportWidth, height: viewportHeight, offsetTop, offsetLeft } = getEffectiveViewport();
|
||||
const horizontalPadding = ACTIVITY_VIEW_MENU_VIEWPORT_PADDING;
|
||||
const verticalPadding = ACTIVITY_VIEW_MENU_VIEWPORT_PADDING;
|
||||
const gap = ACTIVITY_VIEW_MENU_TRIGGER_GAP;
|
||||
const preferredWidth = Math.max(rect.width, ACTIVITY_VIEW_MENU_MIN_WIDTH);
|
||||
const width = Math.min(preferredWidth, Math.max(viewportWidth - horizontalPadding * 2, ACTIVITY_VIEW_MENU_MIN_WIDTH));
|
||||
const triggerTop = rect.top - offsetTop;
|
||||
const triggerBottom = rect.bottom - offsetTop;
|
||||
const triggerLeft = rect.left - offsetLeft;
|
||||
const spaceBelow = viewportHeight - triggerBottom;
|
||||
const spaceAbove = triggerTop;
|
||||
const availableBelow = Math.max(spaceBelow - verticalPadding - gap, ACTIVITY_VIEW_MENU_MIN_HEIGHT);
|
||||
const availableAbove = Math.max(spaceAbove - verticalPadding - gap, ACTIVITY_VIEW_MENU_MIN_HEIGHT);
|
||||
const openUpward = spaceBelow < ACTIVITY_VIEW_MENU_MIN_HEIGHT && spaceAbove > spaceBelow;
|
||||
const maxHeight = Math.max(
|
||||
Math.min(openUpward ? availableAbove : availableBelow, ACTIVITY_VIEW_MENU_MAX_HEIGHT),
|
||||
ACTIVITY_VIEW_MENU_MIN_HEIGHT,
|
||||
);
|
||||
const left = Math.min(
|
||||
Math.max(triggerLeft, horizontalPadding),
|
||||
viewportWidth - horizontalPadding - width,
|
||||
) + offsetLeft;
|
||||
const top = openUpward
|
||||
? Math.max(verticalPadding + offsetTop, triggerTop - maxHeight - gap + offsetTop)
|
||||
: Math.min(
|
||||
triggerBottom + gap + offsetTop,
|
||||
viewportHeight + offsetTop - verticalPadding - maxHeight,
|
||||
);
|
||||
|
||||
setActivityViewMenuPosition({ top, left, minWidth: width, maxHeight });
|
||||
}, [getEffectiveViewport]);
|
||||
|
||||
const activityViewOptions = useMemo<Array<{ value: ActivitySegment; label: string }>>(() => [
|
||||
{ value: "current", label: t("taskDetail.activity.current", "Live") },
|
||||
{ value: "feed", label: t("taskDetail.activity.feed", "Feed") },
|
||||
@@ -2830,6 +2907,8 @@ export function TaskDetailContent({
|
||||
setActiveTab("chat");
|
||||
setActivitySegment(value);
|
||||
setShowActivityViewMenu(false);
|
||||
setActivityViewMenuPosition(null);
|
||||
requestAnimationFrame(() => activityViewButtonRef.current?.focus());
|
||||
}, []);
|
||||
|
||||
const handleActivityTabKeyDown = useCallback((event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
@@ -2850,9 +2929,8 @@ export function TaskDetailContent({
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setShowActivityViewMenu(false);
|
||||
activityViewButtonRef.current?.focus();
|
||||
}, []);
|
||||
closeActivityViewMenuAndFocusTrigger();
|
||||
}, [closeActivityViewMenuAndFocusTrigger]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showMoveMenu) {
|
||||
@@ -2863,21 +2941,100 @@ export function TaskDetailContent({
|
||||
firstMenuItem?.focus();
|
||||
}, [showMoveMenu]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!showActivityViewMenu) {
|
||||
setActivityViewMenuPosition(null);
|
||||
return;
|
||||
}
|
||||
|
||||
updateActivityViewMenuPosition();
|
||||
}, [showActivityViewMenu, updateActivityViewMenuPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showActivityViewMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleViewportChange = () => {
|
||||
setShowActivityViewMenu(false);
|
||||
setActivityViewMenuPosition(null);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleViewportChange);
|
||||
window.addEventListener("orientationchange", handleViewportChange);
|
||||
window.addEventListener("scroll", handleViewportChange, true);
|
||||
const visualViewport = window.visualViewport;
|
||||
visualViewport?.addEventListener("resize", handleViewportChange);
|
||||
visualViewport?.addEventListener("scroll", handleViewportChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleViewportChange);
|
||||
window.removeEventListener("orientationchange", handleViewportChange);
|
||||
window.removeEventListener("scroll", handleViewportChange, true);
|
||||
visualViewport?.removeEventListener("resize", handleViewportChange);
|
||||
visualViewport?.removeEventListener("scroll", handleViewportChange);
|
||||
};
|
||||
}, [showActivityViewMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowActivityViewMenu(false);
|
||||
setActivityViewMenuPosition(null);
|
||||
}, [task.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showActivityViewMenu || !activityViewMenuPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedMenuItem = activityViewMenuRef.current?.querySelector<HTMLButtonElement>(".activity-view-menu-item[aria-current='true']");
|
||||
const firstMenuItem = activityViewMenuRef.current?.querySelector<HTMLButtonElement>(".activity-view-menu-item");
|
||||
(selectedMenuItem ?? firstMenuItem)?.focus();
|
||||
}, [showActivityViewMenu]);
|
||||
}, [showActivityViewMenu, activityViewMenuPosition]);
|
||||
|
||||
const renderActivityViewMenu = () => {
|
||||
if (!showActivityViewMenu || !activityViewMenuPosition || typeof document === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={activityViewMenuRef}
|
||||
className="activity-view-menu"
|
||||
role="menu"
|
||||
aria-label={t("taskDetail.activity.menuLabel", "Activity views")}
|
||||
onKeyDown={handleActivityViewMenuKeyDown}
|
||||
style={{
|
||||
top: activityViewMenuPosition.top,
|
||||
left: activityViewMenuPosition.left,
|
||||
minWidth: activityViewMenuPosition.minWidth,
|
||||
maxHeight: activityViewMenuPosition.maxHeight,
|
||||
}}
|
||||
>
|
||||
{activityViewOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className="activity-view-menu-item"
|
||||
role="menuitem"
|
||||
aria-current={activitySegment === option.value ? "true" : undefined}
|
||||
onClick={() => selectActivityView(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
|
||||
const renderActivityTab = () => (
|
||||
<div className="detail-tab-dropdown" ref={activityViewMenuRef}>
|
||||
<div className="detail-tab-dropdown" ref={activityViewDropdownRef}>
|
||||
{/*
|
||||
FNXC:TaskDetailActivity 2026-06-30-23:59:
|
||||
The top-level Activity tab is the only Activity view dropdown trigger. Keep the stable internal `chat` tab id and `current`/`feed`/`raw-logs` segment ids, but remove the in-panel Activity view select so desktop, embedded, and mobile tab strips have one canonical view switcher.
|
||||
|
||||
FNXC:TaskDetailActivity 2026-07-01-00:00:
|
||||
Mobile task-detail tabs intentionally overflow-scroll horizontally, so the Activity view menu must be root-portaled and viewport-positioned instead of rendered inside `.detail-tabs` where overflow clipping can blank adjacent tabs and content.
|
||||
*/}
|
||||
<button
|
||||
ref={activityViewButtonRef}
|
||||
@@ -2896,22 +3053,7 @@ export function TaskDetailContent({
|
||||
<span>{t("taskDetail.tabs.activity", "Activity")}</span>
|
||||
<ChevronDown className="detail-tab-chevron" aria-hidden="true" />
|
||||
</button>
|
||||
{showActivityViewMenu && (
|
||||
<div className="activity-view-menu" role="menu" aria-label={t("taskDetail.activity.menuLabel", "Activity views")} onKeyDown={handleActivityViewMenuKeyDown}>
|
||||
{activityViewOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className="activity-view-menu-item"
|
||||
role="menuitem"
|
||||
aria-current={activitySegment === option.value ? "true" : undefined}
|
||||
onClick={() => selectActivityView(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{renderActivityViewMenu()}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -16,12 +16,15 @@ describe("TaskDetailModal CSS contract", () => {
|
||||
expect(css).toMatch(/\.detail-tab\s*\{[^}]*flex-shrink\s*:\s*0\s*;/);
|
||||
});
|
||||
|
||||
it("FN-7351 keeps the Activity tab dropdown reachable on narrow task-detail surfaces", async () => {
|
||||
it("FN-7351/FN-7375 keeps the Activity tab dropdown portal-safe on narrow task-detail surfaces", async () => {
|
||||
const css = await loadAllAppCssBaseOnly();
|
||||
|
||||
expect(css).toMatch(/\.detail-tab-dropdown\s*\{[^}]*flex-shrink\s*:\s*0\s*;/);
|
||||
expect(css).toMatch(/\.detail-tab--activity\s*\{[^}]*display\s*:\s*inline-flex\s*;/);
|
||||
expect(css).toMatch(/\.activity-view-menu\s*\{[^}]*min-inline-size\s*:\s*100%\s*;/);
|
||||
expect(css).toMatch(/\.activity-view-menu\s*\{[^}]*position\s*:\s*fixed\s*;/);
|
||||
expect(css).toMatch(/\.activity-view-menu\s*\{[^}]*overflow-y\s*:\s*auto\s*;/);
|
||||
expect(css).not.toMatch(/\.activity-view-menu\s*\{[^}]*position\s*:\s*absolute\s*;/);
|
||||
expect(css).not.toMatch(/\.activity-view-menu\s*\{[^}]*min-inline-size\s*:\s*100%\s*;/);
|
||||
expect(css).not.toContain(".activity-view-select");
|
||||
expect(css).not.toContain(".activity-segmented-control");
|
||||
expect(css).not.toContain(".activity-segment");
|
||||
|
||||
@@ -361,7 +361,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(detailBodyBlock).not.toContain("overflow: hidden;");
|
||||
});
|
||||
|
||||
it("keeps the Activity tab dropdown compact and reachable on mobile", () => {
|
||||
it("keeps the Activity tab dropdown portal-safe and reachable on mobile", () => {
|
||||
const css = readDashboardStylesSource();
|
||||
const tabDropdownBlock = getExactCssRuleBlock(css, ".detail-tab-dropdown");
|
||||
const activityTabBlock = getExactCssRuleBlock(css, ".detail-tab--activity");
|
||||
@@ -373,9 +373,15 @@ describe("TaskDetailModal", () => {
|
||||
expect(tabDropdownBlock).toContain("flex-shrink: 0;");
|
||||
expect(activityTabBlock).toContain("display: inline-flex;");
|
||||
expect(activityTabBlock).toContain("gap: var(--space-xs);");
|
||||
expect(menuBlock).toContain("min-inline-size: 100%;");
|
||||
expect(menuBlock).toContain("position: fixed;");
|
||||
expect(menuBlock).toContain("z-index: 1000;");
|
||||
expect(menuBlock).toContain("padding: var(--space-xs);");
|
||||
expect(mobileMenuBlock).toContain("min-inline-size: calc(100% + var(--space-xl));");
|
||||
expect(menuBlock).toContain("overflow-y: auto;");
|
||||
expect(menuBlock).not.toContain("position: absolute;");
|
||||
expect(menuBlock).not.toContain("inset-block-start");
|
||||
expect(menuBlock).not.toContain("inset-inline-start");
|
||||
expect(menuBlock).not.toContain("min-inline-size: 100%;");
|
||||
expect(mobileMenuBlock).toContain("max-inline-size: calc(100vw - (var(--space-md) * 2));");
|
||||
expect(css).not.toContain(".activity-view-select");
|
||||
expect(css).not.toContain(".activity-segmented-control");
|
||||
expect(css).not.toContain(".activity-segment");
|
||||
|
||||
@@ -137,6 +137,67 @@ describe("TaskDetailModal Activity and planner Chat tab integration", () => {
|
||||
expect(screen.getByText("raw executor line")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("portals the mobile Activity view menu outside the tab scroller while keeping tabs and content visible", () => {
|
||||
const originalInnerWidth = window.innerWidth;
|
||||
const originalInnerHeight = window.innerHeight;
|
||||
const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect;
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: 390 });
|
||||
Object.defineProperty(window, "innerHeight", { configurable: true, value: 844 });
|
||||
HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() {
|
||||
if (this.classList.contains("detail-tab--activity")) {
|
||||
return { x: 24, y: 96, top: 96, right: 116, bottom: 132, left: 24, width: 92, height: 36, toJSON: () => ({}) } as DOMRect;
|
||||
}
|
||||
return originalGetBoundingClientRect.call(this);
|
||||
};
|
||||
|
||||
try {
|
||||
mockRawLogs([
|
||||
{ timestamp: "2026-06-30T20:03:00.000Z", taskId: "FN-7315", type: "text", agent: "executor", text: "raw executor line" },
|
||||
] as AgentLogEntry[]);
|
||||
renderModal();
|
||||
|
||||
const tabs = document.querySelector(".detail-tabs");
|
||||
expect(tabs).not.toBeNull();
|
||||
expect(screen.getByText("Existing steering guidance")).toBeInTheDocument();
|
||||
|
||||
const menu = openActivityViewMenu();
|
||||
expect(menu.parentElement).toBe(document.body);
|
||||
expect(tabs).not.toContainElement(menu);
|
||||
expect(document.querySelector(".detail-tab-dropdown")?.contains(menu)).toBe(false);
|
||||
expect(menu).toHaveStyle({ position: "fixed" });
|
||||
expect(menu.style.top).not.toBe("");
|
||||
expect(menu.style.left).not.toBe("");
|
||||
expect(activityViewLabels()).toEqual(["Live", "Feed", "Raw"]);
|
||||
expect(topLevelTabLabels()).toEqual(expect.arrayContaining(["Activity", "Chat", "Plan", "Changes", "Review"]));
|
||||
expect(screen.getByRole("button", { name: "Chat" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Plan" })).toBeInTheDocument();
|
||||
expect(screen.getByText("Existing steering guidance")).toBeInTheDocument();
|
||||
|
||||
fireEvent.keyDown(menu, { key: "Escape" });
|
||||
expect(screen.queryByRole("menu", { name: "Activity views" })).not.toBeInTheDocument();
|
||||
expect(topLevelTabLabels()).toEqual(expect.arrayContaining(["Activity", "Chat", "Plan", "Changes", "Review"]));
|
||||
expect(screen.getByText("Existing steering guidance")).toBeInTheDocument();
|
||||
|
||||
selectActivityView("feed");
|
||||
expect(screen.getByRole("heading", { name: "Feed" })).toBeInTheDocument();
|
||||
expect(screen.getByText("Posted update")).toBeInTheDocument();
|
||||
expect(topLevelTabLabels()).toEqual(expect.arrayContaining(["Activity", "Chat", "Plan", "Changes", "Review"]));
|
||||
|
||||
selectActivityView("raw-logs");
|
||||
expect(screen.getByTestId("agent-log-viewer")).toBeInTheDocument();
|
||||
expect(screen.getByText("raw executor line")).toBeInTheDocument();
|
||||
|
||||
selectActivityView("current");
|
||||
expect(screen.getByText("Existing steering guidance")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("heading", { name: "Feed" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("agent-log-viewer")).not.toBeInTheDocument();
|
||||
} finally {
|
||||
HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: originalInnerWidth });
|
||||
Object.defineProperty(window, "innerHeight", { configurable: true, value: originalInnerHeight });
|
||||
}
|
||||
});
|
||||
|
||||
it("restores Chat-first ordering and omitted non-done default when the project setting is enabled", () => {
|
||||
mockRawLogs([]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user