fix(planning): use back-only session navigation
This commit is contained in:
7
.changeset/use-planning-session-back.md
Normal file
7
.changeset/use-planning-session-back.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Replace the Planning Sessions toggle with a consistent Back-to-sessions control.
|
||||||
|
category: fix
|
||||||
|
dev: Uses the existing session-list transition across desktop and compact Planning layouts.
|
||||||
@@ -587,11 +587,12 @@ The New session button must look EXACTLY like Missions' primary sidebar create b
|
|||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.planning-mobile-back {
|
.planning-session-back {
|
||||||
/* Visible only on mobile (see media query below). On desktop the sidebar
|
display: inline-flex;
|
||||||
is always visible alongside the detail pane, so a back button is never
|
min-width: calc(var(--space-md) * 2.25);
|
||||||
needed. */
|
min-height: calc(var(--space-md) * 2.25);
|
||||||
display: none;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -602,11 +603,11 @@ The New session button must look EXACTLY like Missions' primary sidebar create b
|
|||||||
transition: background var(--transition-fast), box-shadow var(--transition-fast);
|
transition: background var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.planning-mobile-back:hover {
|
.planning-session-back:hover {
|
||||||
background: var(--card-hover);
|
background: var(--card-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.planning-mobile-back:focus-visible {
|
.planning-session-back:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: var(--focus-ring-strong);
|
box-shadow: var(--focus-ring-strong);
|
||||||
}
|
}
|
||||||
@@ -711,14 +712,6 @@ The New session button must look EXACTLY like Missions' primary sidebar create b
|
|||||||
.planning-modal-body--show-list .planning-detail {
|
.planning-modal-body--show-list .planning-detail {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
/* The rendered Back control needs a full touch target and must not be squeezed out of the title row. */
|
|
||||||
.planning-mobile-back {
|
|
||||||
display: inline-flex;
|
|
||||||
min-width: calc(var(--space-md) * 2.25);
|
|
||||||
min-height: calc(var(--space-md) * 2.25);
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
/* Always keep action buttons visible on mobile (no hover) */
|
/* Always keep action buttons visible on mobile (no hover) */
|
||||||
.planning-sidebar-item-delete,
|
.planning-sidebar-item-delete,
|
||||||
.planning-sidebar-item-archive {
|
.planning-sidebar-item-archive {
|
||||||
@@ -1830,14 +1823,6 @@ plan actions, and a token-sized bottom inset keep all three controls inline with
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.planning-mobile-back {
|
|
||||||
display: inline-flex;
|
|
||||||
min-width: calc(var(--space-md) * 2.25);
|
|
||||||
min-height: calc(var(--space-md) * 2.25);
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.planning-summary-actions {
|
.planning-summary-actions {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -2169,7 +2154,7 @@ mobile and desktop offer the same View task / Return to sessions choices.
|
|||||||
/*
|
/*
|
||||||
FNXC:PlanningModeMobile 2026-07-20-21:50:
|
FNXC:PlanningModeMobile 2026-07-20-21:50:
|
||||||
The Planning detail header keeps Back, the lightbulb, a single-line truncated title, and Edit on
|
The Planning detail header keeps Back, the lightbulb, a single-line truncated title, and Edit on
|
||||||
one row. Fixed icon controls may not shrink; only the title yields space before Sessions/History.
|
one row. Fixed icon controls may not shrink; only the title yields space before History.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.planning-modal--embedded .modal-header--embedded .detail-title-row {
|
.planning-modal--embedded .modal-header--embedded .detail-title-row {
|
||||||
@@ -2191,7 +2176,7 @@ mobile and desktop offer the same View task / Return to sessions choices.
|
|||||||
|
|
||||||
.planning-modal--embedded .modal-header--embedded .detail-title-row > svg,
|
.planning-modal--embedded .modal-header--embedded .detail-title-row > svg,
|
||||||
.planning-modal--embedded .modal-header--embedded .detail-title-row > .btn-icon,
|
.planning-modal--embedded .modal-header--embedded .detail-title-row > .btn-icon,
|
||||||
.planning-modal--embedded .modal-header--embedded .planning-mobile-back {
|
.planning-modal--embedded .modal-header--embedded .planning-session-back {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2202,7 +2187,7 @@ mobile and desktop offer the same View task / Return to sessions choices.
|
|||||||
|
|
||||||
.planning-header-controls {
|
.planning-header-controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: minmax(0, 1fr);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -617,14 +617,13 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
const isCompactInterview = viewportMode !== "desktop" || isShortViewport();
|
const isCompactInterview = viewportMode !== "desktop" || isShortViewport();
|
||||||
/*
|
/*
|
||||||
FNXC:PlanningModeMobile 2026-07-20-10:30:
|
FNXC:PlanningModeMobile 2026-07-20-10:30:
|
||||||
FN-8427 makes the saved-session list a real compact destination. Both Back and Sessions
|
FN-8427 makes the saved-session list a real destination. Back enters this mode and unmounts
|
||||||
enter this one mode, which must unmount the active interview plan so it cannot consume
|
the active interview plan so it cannot consume flex height beneath session rows.
|
||||||
flex height beneath session rows. Desktop preserves its three-pane interview until its
|
|
||||||
explicit Sessions toggle requests the same list destination.
|
|
||||||
*/
|
*/
|
||||||
const isSessionListMode = showSessionList || (isCompactInterview && !mobileShowDetail);
|
const isSessionListMode = showSessionList || (isCompactInterview && !mobileShowDetail);
|
||||||
// FNXC:PlanningModeMobile 2026-07-20-10:30: Empty mobile state opens the composer because no saved destination exists; once sessions exist, every compact detail surface gets this single Back-to-list escape.
|
// FNXC:PlanningSessionBack 2026-07-21-11:15: Back covers selected details on every viewport and drafts whenever saved sessions exist; list mode removes it instead of leaving an orphaned control.
|
||||||
const canReturnToSessionList = isCompactInterview && mobileShowDetail && planningSessions.length > 0;
|
const canReturnToSessionList = !isSessionListMode
|
||||||
|
&& (selectedSessionId !== null || planningSessions.length > 0);
|
||||||
const [isRefineMenuOpen, setIsRefineMenuOpen] = useState(false);
|
const [isRefineMenuOpen, setIsRefineMenuOpen] = useState(false);
|
||||||
const [mobileWorkspaceTab, setMobileWorkspaceTab] = useState<"question" | "plan">("question");
|
const [mobileWorkspaceTab, setMobileWorkspaceTab] = useState<"question" | "plan">("question");
|
||||||
/*
|
/*
|
||||||
@@ -1918,6 +1917,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
}, [projectId, resetDetailState, resumeSessionId, selectedSessionId]);
|
}, [projectId, resetDetailState, resumeSessionId, selectedSessionId]);
|
||||||
|
|
||||||
const handleBackToList = useCallback(() => {
|
const handleBackToList = useCallback(() => {
|
||||||
|
setIsHistoryOpen(false);
|
||||||
setShowSessionList(true);
|
setShowSessionList(true);
|
||||||
setMobileShowDetail(false);
|
setMobileShowDetail(false);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -2832,13 +2832,13 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
<div className={isEmbedded ? "modal modal-lg planning-modal planning-modal--embedded" : "modal modal-lg planning-modal"} ref={modalRef}>
|
<div className={isEmbedded ? "modal modal-lg planning-modal planning-modal--embedded" : "modal modal-lg planning-modal"} ref={modalRef}>
|
||||||
{/*
|
{/*
|
||||||
FNXC:PlanningMode 2026-06-22-00:00:
|
FNXC:PlanningMode 2026-06-22-00:00:
|
||||||
Embedded planning is a main-content destination, not a dialog: it drops the modal close button and renders a plain common title (modal-header--embedded) matching other embedded views like Command Center. The mobile back affordance stays because it navigates the session list, not the view.
|
Embedded planning is a main-content destination, not a dialog: it drops the modal close button and renders a plain common title (modal-header--embedded) matching other embedded views like Command Center. The session-list Back affordance stays because it navigates within Planning, not away from the view.
|
||||||
*/}
|
*/}
|
||||||
<div className={isEmbedded ? "modal-header modal-header--embedded" : "modal-header"}>
|
<div className={isEmbedded ? "modal-header modal-header--embedded" : "modal-header"}>
|
||||||
<div className="detail-title-row">
|
<div className="detail-title-row">
|
||||||
{canReturnToSessionList && (
|
{canReturnToSessionList && (
|
||||||
<button
|
<button
|
||||||
className="modal-back planning-mobile-back"
|
className="modal-back planning-session-back"
|
||||||
onClick={handleBackToList}
|
onClick={handleBackToList}
|
||||||
aria-label={t("planning.backToSessions", "Back to sessions")}
|
aria-label={t("planning.backToSessions", "Back to sessions")}
|
||||||
title={t("planning.backToSessions", "Back to sessions")}
|
title={t("planning.backToSessions", "Back to sessions")}
|
||||||
@@ -2867,26 +2867,13 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/*
|
{/*
|
||||||
FNXC:PlanningModeMobileTablet 2026-07-20-09:12:
|
FNXC:PlanningSessionBack 2026-07-21-11:15:
|
||||||
When the viewport cannot fit three interview panes, operators must still be able to return
|
History remains the only detail action in this group. Session-list navigation lives in the
|
||||||
to the session list and then back to the active question. Keep the list and detail state
|
title-row Back control on every viewport, avoiding a duplicate Sessions toggle and keeping
|
||||||
synchronized on both transitions so a second Sessions press cannot leave the question pane
|
compact list/detail state synchronized through one handler.
|
||||||
hidden by the mobile list CSS.
|
|
||||||
*/}
|
*/}
|
||||||
{selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "error" || view.type === "plan_review" || view.type === "create_retry") && (
|
{selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "error" || view.type === "plan_review" || view.type === "create_retry") && (
|
||||||
<div className="planning-header-controls">
|
<div className="planning-header-controls">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn"
|
|
||||||
onClick={() => {
|
|
||||||
const showList = !isSessionListMode;
|
|
||||||
setIsHistoryOpen(false);
|
|
||||||
setShowSessionList(showList);
|
|
||||||
if (isCompactInterview) setMobileShowDetail(!showList);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("planning.sessions", "Sessions")}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
ref={historyTriggerRef}
|
ref={historyTriggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -115,12 +115,16 @@ describe("PlanningModeModal CSS responsive action contract", () => {
|
|||||||
|
|
||||||
it("uses consistent full-width header controls without crowding the mobile session title", () => {
|
it("uses consistent full-width header controls without crowding the mobile session title", () => {
|
||||||
const css = loadPlanningCss();
|
const css = loadPlanningCss();
|
||||||
|
const backRule = findRule(css, ".planning-session-back");
|
||||||
expect(findRule(css, ".planning-header-controls")).toMatch(/gap\s*:\s*var\(--space-sm\)\s*;/);
|
expect(findRule(css, ".planning-header-controls")).toMatch(/gap\s*:\s*var\(--space-sm\)\s*;/);
|
||||||
expect(findRule(css, ".planning-header-controls .btn")).toMatch(/min-height\s*:\s*calc\(var\(--space-2xl\) \+ var\(--space-sm\)\)\s*;/);
|
expect(findRule(css, ".planning-header-controls .btn")).toMatch(/min-height\s*:\s*calc\(var\(--space-2xl\) \+ var\(--space-sm\)\)\s*;/);
|
||||||
|
expect(backRule).toMatch(/display\s*:\s*inline-flex\s*;/);
|
||||||
|
expect(backRule).toMatch(/min-width\s*:\s*calc\(var\(--space-md\) \* 2\.25\)\s*;/);
|
||||||
|
expect(backRule).toMatch(/min-height\s*:\s*calc\(var\(--space-md\) \* 2\.25\)\s*;/);
|
||||||
|
|
||||||
const mobileCss = getMediaBlocks(css, MOBILE_ACTIONS_QUERY).join("\n");
|
const mobileCss = getMediaBlocks(css, MOBILE_ACTIONS_QUERY).join("\n");
|
||||||
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded")).toMatch(/flex-wrap\s*:\s*wrap\s*;/);
|
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded")).toMatch(/flex-wrap\s*:\s*wrap\s*;/);
|
||||||
expect(findRule(mobileCss, ".planning-header-controls")).toMatch(/grid-template-columns\s*:\s*repeat\(2, minmax\(0, 1fr\)\)\s*;/);
|
expect(findRule(mobileCss, ".planning-header-controls")).toMatch(/grid-template-columns\s*:\s*minmax\(0, 1fr\)\s*;/);
|
||||||
expect(findRule(mobileCss, ".planning-header-controls")).toMatch(/width\s*:\s*100%\s*;/);
|
expect(findRule(mobileCss, ".planning-header-controls")).toMatch(/width\s*:\s*100%\s*;/);
|
||||||
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row")).toMatch(/flex-wrap\s*:\s*nowrap\s*;/);
|
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row")).toMatch(/flex-wrap\s*:\s*nowrap\s*;/);
|
||||||
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row")).toMatch(/overflow\s*:\s*hidden\s*;/);
|
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row")).toMatch(/overflow\s*:\s*hidden\s*;/);
|
||||||
@@ -128,7 +132,7 @@ describe("PlanningModeModal CSS responsive action contract", () => {
|
|||||||
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row h3")).toMatch(/min-width\s*:\s*0\s*;/);
|
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row h3")).toMatch(/min-width\s*:\s*0\s*;/);
|
||||||
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row h3")).toMatch(/text-overflow\s*:\s*ellipsis\s*;/);
|
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row h3")).toMatch(/text-overflow\s*:\s*ellipsis\s*;/);
|
||||||
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row h3")).toMatch(/white-space\s*:\s*nowrap\s*;/);
|
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row h3")).toMatch(/white-space\s*:\s*nowrap\s*;/);
|
||||||
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row > svg,\n .planning-modal--embedded .modal-header--embedded .detail-title-row > .btn-icon,\n .planning-modal--embedded .modal-header--embedded .planning-mobile-back")).toMatch(/flex\s*:\s*0 0 auto\s*;/);
|
expect(findRule(mobileCss, ".planning-modal--embedded .modal-header--embedded .detail-title-row > svg,\n .planning-modal--embedded .modal-header--embedded .detail-title-row > .btn-icon,\n .planning-modal--embedded .modal-header--embedded .planning-session-back")).toMatch(/flex\s*:\s*0 0 auto\s*;/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps tablet question and plan actions on one aligned row with a tight bottom inset", () => {
|
it("keeps tablet question and plan actions on one aligned row with a tight bottom inset", () => {
|
||||||
@@ -173,9 +177,5 @@ describe("PlanningModeModal CSS responsive action contract", () => {
|
|||||||
const footerRule = findRule(mobileShellCss, ".planning-modal-body--show-list .planning-sidebar-footer");
|
const footerRule = findRule(mobileShellCss, ".planning-modal-body--show-list .planning-sidebar-footer");
|
||||||
expect(footerRule).toBeTruthy();
|
expect(footerRule).toBeTruthy();
|
||||||
expect(footerRule).toMatch(/flex-shrink\s*:\s*0\s*;/);
|
expect(footerRule).toMatch(/flex-shrink\s*:\s*0\s*;/);
|
||||||
|
|
||||||
const mobileBackRule = findRule(mobileShellCss, ".planning-mobile-back");
|
|
||||||
expect(mobileBackRule).toMatch(/display\s*:\s*inline-flex\s*;/);
|
|
||||||
expect(mobileBackRule).toMatch(/min-height\s*:\s*calc\(var\(--space-md\) \* 2\.25\)\s*;/);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
describe("PlanningModeModal initial surface", () => {
|
describe("PlanningModeModal initial surface", () => {
|
||||||
it("keeps Sessions as the single escape hatch while the active interview is sequential", () => {
|
it("keeps the sequential interview surfaces available", () => {
|
||||||
const source = readFileSync(resolve(process.cwd(), "app/components/PlanningModeModal.tsx"), "utf8");
|
const source = readFileSync(resolve(process.cwd(), "app/components/PlanningModeModal.tsx"), "utf8");
|
||||||
expect(source).toContain('data-testid="planning-plan-review"');
|
expect(source).toContain('data-testid="planning-plan-review"');
|
||||||
expect(source).toContain('data-testid="planning-refine-menu"');
|
expect(source).toContain('data-testid="planning-refine-menu"');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
|
|||||||
import { PlanningModeModal } from "../PlanningModeModal";
|
import { PlanningModeModal } from "../PlanningModeModal";
|
||||||
import { mockCreatePlanningDraft, mockFetchAiSession, mockFetchAiSessions, mockRespondToPlanning, mockRetryPlanningSession, mockStartPlanningStreaming, mockStopPlanningGeneration, mockValidatePlanningSession, mockCreateTaskFromPlanning, mockTasks, mockSummary } from "./PlanningModeModal.test-helpers";
|
import { mockCreatePlanningDraft, mockFetchAiSession, mockFetchAiSessions, mockRespondToPlanning, mockRetryPlanningSession, mockStartPlanningStreaming, mockStopPlanningGeneration, mockValidatePlanningSession, mockCreateTaskFromPlanning, mockTasks, mockSummary } from "./PlanningModeModal.test-helpers";
|
||||||
|
|
||||||
const mockViewportMode = vi.hoisted(() => vi.fn(() => "desktop" as "desktop" | "mobile"));
|
const mockViewportMode = vi.hoisted(() => vi.fn(() => "desktop" as "desktop" | "tablet" | "mobile"));
|
||||||
const mockConnectPlanningStream = vi.hoisted(() => vi.fn());
|
const mockConnectPlanningStream = vi.hoisted(() => vi.fn());
|
||||||
const mockPlanningSse = vi.hoisted(() => ({ events: null as Record<string, (event: MessageEvent) => void> | null }));
|
const mockPlanningSse = vi.hoisted(() => ({ events: null as Record<string, (event: MessageEvent) => void> | null }));
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ describe("PlanningModeModal sequential flow", () => {
|
|||||||
expect(screen.queryByRole("checkbox", { name: "Security boundaries" })).toBeNull();
|
expect(screen.queryByRole("checkbox", { name: "Security boundaries" })).toBeNull();
|
||||||
expect(screen.getByRole("button", { name: "Refine" })).toBeEnabled();
|
expect(screen.getByRole("button", { name: "Refine" })).toBeEnabled();
|
||||||
expect(screen.getByRole("button", { name: "Proceed with plan" })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: "Proceed with plan" })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: "Sessions" })).toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Sessions" })).toBeNull();
|
||||||
const scrollRegion = screen.getByTestId("planning-plan-scroll");
|
const scrollRegion = screen.getByTestId("planning-plan-scroll");
|
||||||
const actionBar = screen.getByTestId("planning-plan-actions");
|
const actionBar = screen.getByTestId("planning-plan-actions");
|
||||||
expect(scrollRegion).not.toContainElement(actionBar);
|
expect(scrollRegion).not.toContainElement(actionBar);
|
||||||
@@ -301,7 +301,7 @@ describe("PlanningModeModal sequential flow", () => {
|
|||||||
expect(mockConnectPlanningStream).not.toHaveBeenCalled();
|
expect(mockConnectPlanningStream).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens question, answer, and collapsed AI reasoning history beside Sessions", async () => {
|
it("opens question, answer, and collapsed AI reasoning history without a Sessions toggle", async () => {
|
||||||
mockFetchAiSession.mockResolvedValue({
|
mockFetchAiSession.mockResolvedValue({
|
||||||
...base,
|
...base,
|
||||||
status: "awaiting_input",
|
status: "awaiting_input",
|
||||||
@@ -321,9 +321,8 @@ describe("PlanningModeModal sequential flow", () => {
|
|||||||
});
|
});
|
||||||
renderSession();
|
renderSession();
|
||||||
|
|
||||||
const sessionsButton = await screen.findByRole("button", { name: "Sessions" });
|
const historyButton = await screen.findByRole("button", { name: "History" });
|
||||||
const historyButton = screen.getByRole("button", { name: "History" });
|
expect(screen.queryByRole("button", { name: "Sessions" })).toBeNull();
|
||||||
expect(sessionsButton.parentElement).toContainElement(historyButton);
|
|
||||||
fireEvent.click(historyButton);
|
fireEvent.click(historyButton);
|
||||||
|
|
||||||
expect(screen.getByRole("region", { name: "Question and answer history" })).toBeInTheDocument();
|
expect(screen.getByRole("region", { name: "Question and answer history" })).toBeInTheDocument();
|
||||||
@@ -343,6 +342,55 @@ describe("PlanningModeModal sequential flow", () => {
|
|||||||
await waitFor(() => expect(historyButton).toHaveFocus());
|
await waitFor(() => expect(historyButton).toHaveFocus());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:PlanningSessionBack 2026-07-21-11:15:
|
||||||
|
Session detail navigation has one invariant across desktop and compact layouts: Back is the
|
||||||
|
only route to the saved-session list. The former Sessions toggle must not survive as a second
|
||||||
|
affordance, and list mode must not retain an orphaned Back target.
|
||||||
|
*/
|
||||||
|
it.each(["desktop", "tablet", "mobile"] as const)("uses only Back to return to sessions on %s", async (viewport) => {
|
||||||
|
mockViewportMode.mockReturnValue(viewport);
|
||||||
|
mockFetchAiSession.mockResolvedValue({
|
||||||
|
...base,
|
||||||
|
status: "awaiting_input",
|
||||||
|
currentQuestion: JSON.stringify({ id: "q-current", type: "text", question: "What should happen next?" }),
|
||||||
|
result: JSON.stringify(summaryWithRefinements),
|
||||||
|
inputPayload: "{}",
|
||||||
|
});
|
||||||
|
|
||||||
|
renderSession();
|
||||||
|
|
||||||
|
const backButton = await screen.findByRole("button", { name: "Back to sessions" });
|
||||||
|
const modalBody = document.querySelector(".planning-modal-body");
|
||||||
|
expect(screen.queryByRole("button", { name: "Sessions" })).toBeNull();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "History" }));
|
||||||
|
expect(screen.getByRole("region", { name: "Question and answer history" })).toBeInTheDocument();
|
||||||
|
fireEvent.click(backButton);
|
||||||
|
|
||||||
|
expect(modalBody).toHaveClass("planning-modal-body--show-list");
|
||||||
|
expect(screen.queryByRole("button", { name: "Back to sessions" })).toBeNull();
|
||||||
|
expect(screen.queryByRole("region", { name: "Question and answer history" })).toBeNull();
|
||||||
|
expect(screen.getByRole("complementary", { name: "Planning sessions" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["desktop", "tablet", "mobile"] as const)("keeps Back available from a new-session draft with saved sessions on %s", async (viewport) => {
|
||||||
|
mockViewportMode.mockReturnValue(viewport);
|
||||||
|
mockFetchAiSessions.mockResolvedValue([{
|
||||||
|
...base,
|
||||||
|
type: "planning",
|
||||||
|
status: "awaiting_input",
|
||||||
|
preview: "Saved plan",
|
||||||
|
}]);
|
||||||
|
|
||||||
|
render(<PlanningModeModal isOpen onClose={vi.fn()} onTaskCreated={vi.fn()} onTasksCreated={vi.fn()} tasks={mockTasks} projectId="project-1" />);
|
||||||
|
|
||||||
|
if (viewport !== "desktop") {
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "New session" }));
|
||||||
|
}
|
||||||
|
expect(await screen.findByRole("button", { name: "Back to sessions" })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: "Sessions" })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("creates the task directly and offers task and session-list handoffs", async () => {
|
it("creates the task directly and offers task and session-list handoffs", async () => {
|
||||||
mockFetchAiSession.mockResolvedValue({
|
mockFetchAiSession.mockResolvedValue({
|
||||||
...base,
|
...base,
|
||||||
|
|||||||
Reference in New Issue
Block a user