FN-8427: restore mobile planning session navigation
Restore a full-pane saved-session destination for compact Planning Mode. - Unmount the running-plan pane while mobile and tablet session lists are open. - Keep Back and Sessions available from active, completed, and new planning surfaces. - Cover compact navigation behavior and document the mobile interaction. Files changed: .../fn-8427-planning-mobile-session-list-nav.md | 7 ++ MOBILE.md | 2 +- docs/dashboard-guide.md | 2 +- .../dashboard/app/components/PlanningModeModal.css | 56 +++++++++++++++- .../dashboard/app/components/PlanningModeModal.tsx | 33 ++++++--- .../__tests__/PlanningModeModal.css.test.ts | 5 ++ .../PlanningModeModal.ui-interactions.test.tsx | 78 ++++++++++++++++++++++ 7 files changed, 167 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-8427 Fusion-Task-Lineage: 6664d075-0614-4b5a-ba36-34a0ec6a20a3 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8427-planning-mobile-session-list-nav.md
Normal file
7
.changeset/fn-8427-planning-mobile-session-list-nav.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Restore mobile navigation back to the Planning session list without a stuck Running plan screen.
|
||||||
|
category: fix
|
||||||
|
dev: Hide RunningPlanPane in list mode; ensure Back/Sessions on non-list mobile Planning surfaces.
|
||||||
@@ -101,7 +101,7 @@ gesture delivery enabled across `cap sync` regenerations.
|
|||||||
|
|
||||||
### Planning Mode
|
### Planning Mode
|
||||||
|
|
||||||
Planning Mode opens directly into the composer pane on mobile when no planning sessions exist, avoiding an empty-sidebar dead end. Desktop keeps its three-pane interview (answered history, current question, and running plan) when there is room. Mobile, phone-class short landscape, and tablet use a progressive interview: **Question** is the default full-width surface, while labeled **Running plan** and **Answered questions** controls switch to those surfaces without stacking three panes. **Sessions** and the mobile back control always return to the saved-session list; **Validate plan** remains on the Running plan surface. Once sessions are saved, mobile shows the session list as usual and the user can navigate between list and detail panes.
|
Planning Mode opens directly into the composer pane on mobile when no planning sessions exist, avoiding an empty-sidebar dead end. Desktop keeps its three-pane interview (answered history, current question, and running plan) when there is room. Mobile, phone-class short landscape, and tablet use a progressive interview: **Question** is the default full-width surface, while labeled **Running plan** and **Answered questions** controls switch to those surfaces without stacking three panes. **Sessions** and the mobile back control always return to the saved-session list; this is a full-pane, scrollable list with **New session** as its footer and no Running plan stacked beneath it. The same escape is available from interview, summary, breakdown, and a new-session composer when saved sessions exist; **Validate plan** remains on the Running plan surface.
|
||||||
|
|
||||||
### Chat and Quick Chat mobile scroll/readability behavior
|
### Chat and Quick Chat mobile scroll/readability behavior
|
||||||
|
|
||||||
|
|||||||
@@ -529,7 +529,7 @@ When an active Planning AI generation appears stuck, Planning Mode automatically
|
|||||||
Use **New session** to restart planning with a different idea.
|
Use **New session** to restart planning with a different idea.
|
||||||
|
|
||||||
<!-- FNXC:PlanningMode 2026-07-18-16:00: Planning Mode is an infinite, user-controlled interview. Each answer updates the running plan and produces another context-aware high-impact question; only Validate plan finalizes it. -->
|
<!-- FNXC:PlanningMode 2026-07-18-16:00: Planning Mode is an infinite, user-controlled interview. Each answer updates the running plan and produces another context-aware high-impact question; only Validate plan finalizes it. -->
|
||||||
Planning Mode keeps the running plan visible beside answered-question history and the current question on desktop; you can rename a session and keep asking high-impact, context-aware questions until you choose **Validate plan**. On tablet, mobile, and phone-class short landscape, the interview switches between labeled **Question**, **Running plan**, and **Answered questions** surfaces so the current question stays usable instead of competing with three columns. **Sessions** (and mobile Back) return to the saved-session list, while **Validate plan** remains available on the Running plan surface. The running title, description, and deliverables are available throughout the interview—including while the next question is generating or a recoverable error is shown. The AI never ends an interview on its own. Selection questions provide alternatives with pros and cons plus an **Other** free-text choice, whose wording follows your input language and whose answer steers the next question. You may edit an earlier answer by question ID without losing later answers; Planning re-derives the running plan and appends a fresh next question.
|
Planning Mode keeps the running plan visible beside answered-question history and the current question on desktop; you can rename a session and keep asking high-impact, context-aware questions until you choose **Validate plan**. On tablet, mobile, and phone-class short landscape, the interview switches between labeled **Question**, **Running plan**, and **Answered questions** surfaces so the current question stays usable instead of competing with three columns. **Sessions** (and mobile Back) return to a full-pane, scrollable saved-session list with **New session** pinned as its footer; the Running plan is not stacked under that list. This escape remains available from interview, summary, breakdown, and a new-session composer whenever saved sessions exist, while **Validate plan** remains available on the Running plan surface. The running title, description, and deliverables are available throughout the interview—including while the next question is generating or a recoverable error is shown. The AI never ends an interview on its own. Selection questions provide alternatives with pros and cons plus an **Other** free-text choice, whose wording follows your input language and whose answer steers the next question. You may edit an earlier answer by question ID without losing later answers; Planning re-derives the running plan and appends a fresh next question.
|
||||||
|
|
||||||
Choose **Validate plan** when the running plan is ready for task creation. Validation is durable and is required before **Create task**, **Create tasks**, or **Start breakdown**; those actions reject unvalidated sessions.
|
Choose **Validate plan** when the running plan is ready for task creation. Validation is durable and is required before **Create task**, **Create tasks**, or **Start breakdown**; those actions reject unvalidated sessions.
|
||||||
|
|
||||||
|
|||||||
@@ -469,6 +469,7 @@ The New session button must look EXACTLY like Missions' primary sidebar create b
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: var(--space-xs);
|
padding: var(--space-xs);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
flex-shrink: 0;
|
||||||
transition: background var(--transition-fast), box-shadow var(--transition-fast);
|
transition: background var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,13 +621,24 @@ The New session button must look EXACTLY like Missions' primary sidebar create b
|
|||||||
.planning-modal-body--show-detail .planning-sidebar {
|
.planning-modal-body--show-detail .planning-sidebar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.planning-modal-body--show-list .planning-detail {
|
/*
|
||||||
|
FNXC:PlanningModeMobile 2026-07-20-10:30:
|
||||||
|
FN-8427 gives session-list mode exclusive ownership of the compact body. JS unmounts
|
||||||
|
RunningPlanPane; this selector is a layout backstop for future render paths so session
|
||||||
|
rows stay scrollable above the New session footer instead of sharing height with a plan.
|
||||||
|
*/
|
||||||
|
.planning-modal-body--show-list .planning-detail,
|
||||||
|
.planning-modal-body--show-list .planning-running-plan,
|
||||||
|
.planning-modal-body--show-list .planning-compact-pane-switcher {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
/* Show back button on mobile whenever it's rendered (the React component
|
/* The rendered Back control needs a full touch target and must not be squeezed out of the title row. */
|
||||||
only renders it when `mobileShowDetail` is true). */
|
|
||||||
.planning-mobile-back {
|
.planning-mobile-back {
|
||||||
display: inline-flex;
|
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,
|
||||||
@@ -1535,6 +1547,44 @@ Tablet embedded Planning keeps the desktop two-pane shell, so the summary footer
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FNXC:PlanningModeMobile 2026-07-20-10:30: tablet uses the same compact list destination as phone and short shells, so summary/compose detail cannot remain beside saved session rows. */
|
||||||
|
.planning-modal-body--show-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planning-modal-body--show-list .planning-sidebar {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planning-modal-body--show-list .planning-sidebar-list {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planning-modal-body--show-list .planning-sidebar-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planning-modal-body--show-list .planning-detail,
|
||||||
|
.planning-modal-body--show-list .planning-running-plan,
|
||||||
|
.planning-modal-body--show-list .planning-compact-pane-switcher {
|
||||||
|
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;
|
||||||
|
|||||||
@@ -521,6 +521,16 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
temporary while a keyboard is open, rather than a global viewport-mode change.
|
temporary while a keyboard is open, rather than a global viewport-mode change.
|
||||||
*/
|
*/
|
||||||
const isCompactInterview = viewportMode !== "desktop" || isShortViewport();
|
const isCompactInterview = viewportMode !== "desktop" || isShortViewport();
|
||||||
|
/*
|
||||||
|
FNXC:PlanningModeMobile 2026-07-20-10:30:
|
||||||
|
FN-8427 makes the saved-session list a real compact destination. Both Back and Sessions
|
||||||
|
enter this one mode, which must unmount the active interview plan so it cannot consume
|
||||||
|
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);
|
||||||
|
// 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.
|
||||||
|
const canReturnToSessionList = isCompactInterview && mobileShowDetail && planningSessions.length > 0;
|
||||||
const [compactInterviewPane, setCompactInterviewPane] = useState<"question" | "plan" | "history">("question");
|
const [compactInterviewPane, setCompactInterviewPane] = useState<"question" | "plan" | "history">("question");
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const { pushNav } = useNavigationHistoryContext();
|
const { pushNav } = useNavigationHistoryContext();
|
||||||
@@ -1192,6 +1202,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
liveGenerationSessionIdRef.current = sessionId;
|
liveGenerationSessionIdRef.current = sessionId;
|
||||||
setSelectedSessionId(sessionId);
|
setSelectedSessionId(sessionId);
|
||||||
setShowSessionList(false);
|
setShowSessionList(false);
|
||||||
|
setMobileShowDetail(true);
|
||||||
|
|
||||||
connectToPlanningStream(sessionId);
|
connectToPlanningStream(sessionId);
|
||||||
setResponseHistory([]);
|
setResponseHistory([]);
|
||||||
@@ -2215,7 +2226,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
*/}
|
*/}
|
||||||
<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">
|
||||||
{mobileShowDetail && (
|
{canReturnToSessionList && (
|
||||||
<button
|
<button
|
||||||
className="modal-back planning-mobile-back"
|
className="modal-back planning-mobile-back"
|
||||||
onClick={handleBackToList}
|
onClick={handleBackToList}
|
||||||
@@ -2257,7 +2268,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
type="button"
|
type="button"
|
||||||
className="btn"
|
className="btn"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const showList = !showSessionList;
|
const showList = !isSessionListMode;
|
||||||
setShowSessionList(showList);
|
setShowSessionList(showList);
|
||||||
if (isCompactInterview) setMobileShowDetail(!showList);
|
if (isCompactInterview) setMobileShowDetail(!showList);
|
||||||
}}
|
}}
|
||||||
@@ -2276,14 +2287,14 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={`planning-modal-body planning-modal-body--split ${
|
className={`planning-modal-body planning-modal-body--split ${
|
||||||
mobileShowDetail ? "planning-modal-body--show-detail" : "planning-modal-body--show-list"
|
isSessionListMode ? "planning-modal-body--show-list" : "planning-modal-body--show-detail"
|
||||||
} ${
|
} ${
|
||||||
selectedSessionId && !showSessionList && (view.type === "question" || view.type === "loading" || view.type === "error") && isCompactInterview
|
selectedSessionId && !isSessionListMode && (view.type === "question" || view.type === "loading" || view.type === "error") && isCompactInterview
|
||||||
? `planning-modal-body--compact-interview planning-modal-body--compact-${compactInterviewPane}`
|
? `planning-modal-body--compact-interview planning-modal-body--compact-${compactInterviewPane}`
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{selectedSessionId && !showSessionList && (view.type === "question" || view.type === "loading" || view.type === "error") ? (
|
{selectedSessionId && !isSessionListMode && (view.type === "question" || view.type === "loading" || view.type === "error") ? (
|
||||||
<AnsweredQuestionHistory
|
<AnsweredQuestionHistory
|
||||||
entries={conversationHistory}
|
entries={conversationHistory}
|
||||||
selectedQuestionId={editingQuestionId ?? (view.type === "question" ? view.session.currentQuestion?.id : undefined)}
|
selectedQuestionId={editingQuestionId ?? (view.type === "question" ? view.session.currentQuestion?.id : undefined)}
|
||||||
@@ -2327,7 +2338,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedSessionId && !showSessionList && (view.type === "question" || view.type === "loading" || view.type === "error") && isCompactInterview && (
|
{selectedSessionId && !isSessionListMode && (view.type === "question" || view.type === "loading" || view.type === "error") && isCompactInterview && (
|
||||||
<nav className="planning-compact-pane-switcher" aria-label={t("planning.interviewPanels", "Planning interview panels")}>
|
<nav className="planning-compact-pane-switcher" aria-label={t("planning.interviewPanels", "Planning interview panels")}>
|
||||||
<button type="button" className={`btn ${compactInterviewPane === "question" ? "btn-primary" : ""}`} aria-pressed={compactInterviewPane === "question"} onClick={() => setCompactInterviewPane("question")}>
|
<button type="button" className={`btn ${compactInterviewPane === "question" ? "btn-primary" : ""}`} aria-pressed={compactInterviewPane === "question"} onClick={() => setCompactInterviewPane("question")}>
|
||||||
{t("planning.question", "Question")}
|
{t("planning.question", "Question")}
|
||||||
@@ -2653,12 +2664,12 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
FNXC:PlanningMode 2026-07-19-15:45:
|
FNXC:PlanningModeMobile 2026-07-20-10:30:
|
||||||
Keep the running-plan pane mounted for an active session while a question is loading or an
|
FN-8427 keeps the running plan through question, loading, and recoverable-error detail
|
||||||
error is recoverable. A session selection is the stable identity across those view states;
|
states, but unmounts it in session-list mode. The list must own the entire compact body;
|
||||||
tying this pane to only the question view recreated the old dead-end interface.
|
leaving this pane mounted reduced rows to the New session footer on mobile.
|
||||||
*/}
|
*/}
|
||||||
{selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "error") && (
|
{selectedSessionId && !isSessionListMode && (view.type === "question" || view.type === "loading" || view.type === "error") && (
|
||||||
<RunningPlanPane
|
<RunningPlanPane
|
||||||
summary={runningSummary}
|
summary={runningSummary}
|
||||||
fallbackDescription={activePlanPrompt}
|
fallbackDescription={activePlanPrompt}
|
||||||
|
|||||||
@@ -112,5 +112,10 @@ 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*;/);
|
||||||
|
|
||||||
|
expect(mobileShellCss).toMatch(/\.planning-modal-body--show-list \.planning-running-plan[\s\S]*?display\s*:\s*none\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*;/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -946,6 +946,84 @@ describe("PlanningModeModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("mobile session-list navigation (FN-8427)", () => {
|
||||||
|
const sessions = [
|
||||||
|
{ id: "session-active", type: "planning", status: "awaiting_input", title: "Active planning session", projectId: null, updatedAt: "2026-07-20T00:00:00.000Z" },
|
||||||
|
{ id: "session-other", type: "planning", status: "complete", title: "Other planning session", projectId: null, updatedAt: "2026-07-20T00:00:00.000Z" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function mockActiveSession() {
|
||||||
|
mockFetchAiSessions.mockResolvedValue(sessions);
|
||||||
|
mockFetchAiSession.mockResolvedValue({
|
||||||
|
id: "session-active",
|
||||||
|
type: "planning",
|
||||||
|
status: "awaiting_input",
|
||||||
|
title: "Active planning session",
|
||||||
|
inputPayload: JSON.stringify({ initialPlan: "Restore navigation" }),
|
||||||
|
conversationHistory: "[]",
|
||||||
|
currentQuestion: JSON.stringify(mockQuestion),
|
||||||
|
result: JSON.stringify(mockSummary),
|
||||||
|
thinkingOutput: "",
|
||||||
|
projectId: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("returns an active mobile interview to usable session rows without a running plan", async () => {
|
||||||
|
mockViewport("mobile");
|
||||||
|
mockActiveSession();
|
||||||
|
const { container } = render(<PlanningModeModal isOpen={true} onClose={mockOnClose} onTaskCreated={mockOnTaskCreated} onTasksCreated={vi.fn()} tasks={mockTasks} resumeSessionId="session-active" initialSessions={sessions} />);
|
||||||
|
|
||||||
|
await screen.findByText("What is the scope?");
|
||||||
|
expect(screen.getByLabelText("Running plan")).toBeInTheDocument();
|
||||||
|
const back = await screen.findByLabelText("Back to sessions");
|
||||||
|
expect(back).toBeEnabled();
|
||||||
|
fireEvent.click(back);
|
||||||
|
|
||||||
|
expect(container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--show-list");
|
||||||
|
expect(screen.getByRole("button", { name: /Other planning session/ })).toBeVisible();
|
||||||
|
expect(screen.queryByLabelText("Running plan")).toBeNull();
|
||||||
|
expect(screen.getByRole("button", { name: "New session" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps desktop running-plan interview chrome without mobile back navigation", async () => {
|
||||||
|
mockViewport("desktop");
|
||||||
|
mockActiveSession();
|
||||||
|
const { container } = render(<PlanningModeModal isOpen={true} onClose={mockOnClose} onTaskCreated={mockOnTaskCreated} onTasksCreated={vi.fn()} tasks={mockTasks} resumeSessionId="session-active" initialSessions={sessions} />);
|
||||||
|
|
||||||
|
await screen.findByText("What is the scope?");
|
||||||
|
expect(screen.getByLabelText("Running plan")).toBeInTheDocument();
|
||||||
|
expect(container.querySelector(".planning-mobile-back")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a mobile list escape from summary and new-session compose when sessions exist", async () => {
|
||||||
|
mockViewport("mobile");
|
||||||
|
mockFetchAiSessions.mockResolvedValue(sessions);
|
||||||
|
mockFetchAiSession.mockResolvedValue({
|
||||||
|
id: "session-active",
|
||||||
|
type: "planning",
|
||||||
|
status: "complete",
|
||||||
|
title: "Active planning session",
|
||||||
|
inputPayload: JSON.stringify({ initialPlan: "Restore navigation", validated: true }),
|
||||||
|
conversationHistory: "[]",
|
||||||
|
currentQuestion: null,
|
||||||
|
result: JSON.stringify(mockSummary),
|
||||||
|
thinkingOutput: "",
|
||||||
|
projectId: null,
|
||||||
|
});
|
||||||
|
const { container } = render(<PlanningModeModal isOpen={true} onClose={mockOnClose} onTaskCreated={mockOnTaskCreated} onTasksCreated={vi.fn()} tasks={mockTasks} resumeSessionId="session-active" initialSessions={sessions} />);
|
||||||
|
|
||||||
|
await screen.findByText("Planning Complete!");
|
||||||
|
fireEvent.click(screen.getByLabelText("Back to sessions"));
|
||||||
|
expect(screen.getByRole("button", { name: /Other planning session/ })).toBeVisible();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "New session" }));
|
||||||
|
expect(screen.getByPlaceholderText(/e.g., Build a user authentication/)).toBeVisible();
|
||||||
|
fireEvent.click(screen.getByLabelText("Back to sessions"));
|
||||||
|
expect(container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--show-list");
|
||||||
|
expect(screen.getByRole("button", { name: /Other planning session/ })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:Planning 2026-06-23-02:00:
|
FNXC:Planning 2026-06-23-02:00:
|
||||||
The embedded Planning sidebar is resizable like Missions: a desktop-only drag handle (role=separator) drives an inline width on .planning-sidebar that persists to localStorage and is clamped to the PLANNING_SIDEBAR_MIN/MAX range. These tests assert the handle exists on desktop, persists a clamped width on arrow-key resize, restores from localStorage, and is absent on mobile (where the sidebar stacks full-width).
|
The embedded Planning sidebar is resizable like Missions: a desktop-only drag handle (role=separator) drives an inline width on .planning-sidebar that persists to localStorage and is clamped to the PLANNING_SIDEBAR_MIN/MAX range. These tests assert the handle exists on desktop, persists a clamped width on arrow-key resize, restores from localStorage, and is absent on mobile (where the sidebar stacks full-width).
|
||||||
|
|||||||
Reference in New Issue
Block a user