fix(dashboard): one Planning add-comment trigger, shown when the selection is done

The plan review pane rendered both the document-adjacent and the action-rail
"Add comment to selection" triggers, so operators saw duplicate buttons. Delete
the document variant and its --document/--mobile CSS pair; the rail button is
now the single control at every breakpoint.

Selection capture also ran on every mid-drag selectionchange, which mounted and
unmounted the trigger as the user dragged. Gate quote writes between pointerdown
and pointerup inside the plan document so the control appears once, on release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-25 10:02:02 -07:00
parent 10df734bd1
commit e4fb3f994e
7 changed files with 188 additions and 100 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Planning Mode shows one "Add comment to selection" button, and only once the selection is finished.
category: fix
dev: Removes the `planning-add-comment--document` trigger and the `--mobile` modifier (single `.planning-add-comment` rail button at every breakpoint); `planSelectionDragActiveRef` suppresses quote writes between pointerdown and pointerup inside the plan document so mid-drag `selectionchange` no longer mounts/unmounts the trigger.

View File

@@ -1798,15 +1798,17 @@ Contextual comments remain transient review controls: render the capture/editor
selection, and render the tray only after a valid comment exists so an empty plan review keeps
its established Refine and Proceed action hierarchy.
*/
/*
FNXC:PlanningComments 2026-07-25-10:20:
Single Add-comment trigger, living in the plan action rail at every breakpoint. The former
--document / --mobile variant pair is gone: both rendered in the DOM and read as duplicate buttons.
*/
.planning-add-comment {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
margin-top: var(--space-lg);
}
.planning-add-comment--mobile {
display: none;
margin-top: 0;
}
.planning-comment-editor,
@@ -1910,17 +1912,11 @@ reach past Proceed — match the tablet in-flow stack on phone too.
}
/*
FNXC:PlanningComments 2026-07-24-05:35:
Tablet and phone hide the document-adjacent trigger (it sits at the end of a long plan and
requires scrolling). The action-rail variant becomes the sole control and spans the full
footer width above Refine/Proceed so a selection always exposes Add comment without leaving
the plan action baseline.
FNXC:PlanningComments 2026-07-25-10:20:
Tablet and phone give the sole action-rail trigger the full footer width above Refine/Proceed so a
selection always exposes Add comment without leaving the plan action baseline.
*/
.planning-add-comment--document {
display: none;
}
.planning-plan-actions .btn.planning-add-comment--mobile {
.planning-plan-actions .btn.planning-add-comment {
display: flex;
grid-column: 1 / -1;
margin-top: 0;
@@ -1934,7 +1930,7 @@ reach past Proceed — match the tablet in-flow stack on phone too.
tablet and phone share the mobile icon scale (default lucide size can dwarf the label in the
full-width rail row).
*/
.planning-plan-actions .btn.planning-add-comment--mobile svg {
.planning-plan-actions .btn.planning-add-comment svg {
width: var(--space-lg);
height: var(--space-lg);
flex-shrink: 0;
@@ -2089,7 +2085,7 @@ plan actions, and a token-sized bottom inset keep all three controls inline with
gap: var(--space-md);
}
.planning-plan-actions .btn.planning-add-comment--mobile {
.planning-plan-actions .btn.planning-add-comment {
display: flex;
grid-column: 1 / -1;
width: 100%;
@@ -2098,7 +2094,7 @@ plan actions, and a token-sized bottom inset keep all three controls inline with
gap: var(--space-sm);
}
.planning-plan-actions .btn.planning-add-comment--mobile svg {
.planning-plan-actions .btn.planning-add-comment svg {
width: var(--space-lg);
height: var(--space-lg);
flex-shrink: 0;

View File

@@ -766,10 +766,17 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
const commentInputRef = useRef<HTMLTextAreaElement>(null);
const commentEditorRef = useRef<HTMLDivElement>(null);
const addCommentTriggerRef = useRef<HTMLButtonElement>(null);
const mobileAddCommentTriggerRef = useRef<HTMLButtonElement>(null);
const restoreCommentTriggerFocusRef = useRef(false);
const isCommentEditorOpenRef = useRef(false);
const pendingOpenCommentQuoteRef = useRef<string | null>(null);
/*
FNXC:PlanningComments 2026-07-25-10:20:
While a pointer drag is extending a plan selection, selectionchange fires on every mouse move.
Mounting/unmounting the Add-comment control on each of those intermediate ranges made the button
strobe under the cursor on desktop. This ref suppresses quote writes for the duration of the drag
so the control appears exactly once, when the selection is done (pointerup/pointercancel).
*/
const planSelectionDragActiveRef = useRef(false);
const setCommentEditorOpen = useCallback((open: boolean) => {
/*
@@ -818,12 +825,12 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
const focusAddCommentTrigger = useCallback(() => {
/*
FNXC:PlanningComments 2026-07-24-05:35:
Tablet and phone both expose the action-rail trigger; only wide desktop uses the document
variant. Match the 1024px CSS gate so focus restore lands on the visible control.
FNXC:PlanningComments 2026-07-25-10:20:
One trigger at every breakpoint. The document-adjacent duplicate that sat at the end of the plan
text was removed (FN operator report: two "Add comment to selection" buttons), so focus restore
always targets the plan action rail control.
*/
const usesRailTrigger = window.matchMedia?.("(max-width: 1024px)").matches ?? false;
(usesRailTrigger ? mobileAddCommentTriggerRef : addCommentTriggerRef).current?.focus();
addCommentTriggerRef.current?.focus();
}, []);
/*
@@ -834,7 +841,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
if (isCommentEditorOpen || !restoreCommentTriggerFocusRef.current) return;
restoreCommentTriggerFocusRef.current = false;
queueMicrotask(() => {
if (addCommentTriggerRef.current || mobileAddCommentTriggerRef.current) {
if (addCommentTriggerRef.current) {
focusAddCommentTrigger();
}
});
@@ -2999,6 +3006,12 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
as the open gesture collapsed the native range.
*/
if (isCommentEditorOpenRef.current || pendingOpenCommentQuoteRef.current) return;
/*
FNXC:PlanningComments 2026-07-25-10:20:
Mid-drag ranges are not a finished selection. Skip them entirely; the pointerup handler runs one
final capture so the control shows once the selection is done instead of flickering per movement.
*/
if (planSelectionDragActiveRef.current) return;
const selection = window.getSelection();
const root = planDocumentRef.current;
@@ -3018,12 +3031,38 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
setSelectedPlanQuote(quote || null);
}, []);
/*
FNXC:PlanningComments 2026-07-25-10:20:
Selection-in-progress gate. A drag that starts inside the plan document hides any stale control and
freezes quote updates until the pointer is released; the release (or cancel) performs the single
capture. Drags that start outside the plan are left alone — their collapse still clears the quote
through the normal selectionchange path.
*/
useEffect(() => {
const handlePointerDown = (event: PointerEvent) => {
if (isCommentEditorOpenRef.current || pendingOpenCommentQuoteRef.current) return;
const root = planDocumentRef.current;
if (!root || !root.contains(event.target as Node)) return;
planSelectionDragActiveRef.current = true;
setSelectedPlanQuote(null);
};
const handlePointerRelease = () => {
if (!planSelectionDragActiveRef.current) return;
planSelectionDragActiveRef.current = false;
capturePlanSelection();
};
document.addEventListener("pointerdown", handlePointerDown);
document.addEventListener("pointerup", handlePointerRelease);
document.addEventListener("pointercancel", handlePointerRelease);
document.addEventListener("selectionchange", capturePlanSelection);
document.addEventListener("mouseup", capturePlanSelection);
document.addEventListener("touchend", capturePlanSelection);
document.addEventListener("keyup", capturePlanSelection);
return () => {
document.removeEventListener("pointerdown", handlePointerDown);
document.removeEventListener("pointerup", handlePointerRelease);
document.removeEventListener("pointercancel", handlePointerRelease);
document.removeEventListener("selectionchange", capturePlanSelection);
document.removeEventListener("mouseup", capturePlanSelection);
document.removeEventListener("touchend", capturePlanSelection);
@@ -3353,20 +3392,13 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
testId="planning-plan-markdown"
/>
</div>
{selectedPlanQuote && !isCommentEditorOpen && (
<button
ref={addCommentTriggerRef}
type="button"
className="btn planning-add-comment planning-add-comment--document"
onMouseDown={(event) => event.preventDefault()}
onPointerDown={handleOpenCommentEditorPointerDown}
onClick={openCommentEditor}
>
{/* FNXC:PlanningComments 2026-07-24-05:55: Match New-session / mobile rail glyph scale. */}
<MessageSquarePlus size={16} aria-hidden="true" />
{t("planning.addComment", "Add comment to selection")}
</button>
)}
{/*
FNXC:PlanningComments 2026-07-25-10:20:
The document-adjacent Add-comment trigger was REMOVED. It duplicated the plan action rail
control (operators saw two "Add comment to selection" buttons) and, sitting at the end of a
long plan, it was the harder of the two to reach. The rail trigger below is now the single
control at every breakpoint. Do not reintroduce a second trigger inside the plan document.
*/}
{isCommentEditorOpen && openCommentQuote && (
<div
ref={commentEditorRef}
@@ -3406,26 +3438,20 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
</div>
<div className="planning-actions planning-summary-actions planning-plan-actions" data-testid="planning-plan-actions">
{/*
FNXC:PlanningComments 2026-07-31-00:00:
FN-8533 keeps the selection-adjacent control on wide desktop, but compact shells need a
counterpart that cannot be lost under the document fold.
FNXC:PlanningComments 2026-07-24-05:50:
On tablet and phone (≤1024) the rail control stays in the plan action footer as a
full-width row above Refine/Proceed so a selection never requires scrolling past the
action baseline. Document-level selectionchange still dismisses it when the selection
collapses. CSS shows exactly one of the two variants; only established 768px/1024px
breakpoint literals are allowed here, while all other dimensions remain design-token based.
FNXC:PlanningComments 2026-07-25-10:20:
The plan action rail holds the ONLY Add-comment trigger, at every breakpoint. It cannot be
lost under the document fold, and a selection never requires scrolling past the action
baseline. Document-level selectionchange still dismisses it when the selection collapses.
FNXC:PlanningComments 2026-07-24-05:55:
Tablet must keep the two-column grid (not flex nowrap) so Add comment stays a full-width
first row with the same MessageSquarePlus 16px glyph as phone.
Tablet and phone keep the two-column grid (not flex nowrap) so Add comment stays a full-width
first row above Refine/Proceed with the same MessageSquarePlus 16px glyph.
*/}
{selectedPlanQuote && !isCommentEditorOpen && (
<button
ref={mobileAddCommentTriggerRef}
ref={addCommentTriggerRef}
type="button"
className="btn planning-add-comment planning-add-comment--mobile"
className="btn planning-add-comment"
onMouseDown={(event) => event.preventDefault()}
onPointerDown={handleOpenCommentEditorPointerDown}
onClick={openCommentEditor}

View File

@@ -165,25 +165,31 @@ describe("PlanningModeModal CSS responsive action contract", () => {
// FNXC:PlanningComments 2026-07-24-05:55: tablet keeps the 2-col grid so Add comment can span above Refine/Proceed.
expectSomeRule(tabletCss, ".planning-plan-actions", /display\s*:\s*grid\s*;/);
expectSomeRule(tabletCss, ".planning-plan-actions", /grid-template-columns\s*:\s*repeat\(2, minmax\(0, 1fr\)\)\s*;/);
expect(findRule(tabletCss, ".planning-plan-actions .btn.planning-add-comment--mobile")).toMatch(/grid-column\s*:\s*1\s*\/\s*-1\s*;/);
expect(findRule(tabletCss, ".planning-plan-actions .btn.planning-add-comment--mobile svg")).toMatch(/width\s*:\s*var\(--space-lg\)\s*;/);
expect(findRule(tabletCss, ".planning-plan-actions .btn.planning-add-comment")).toMatch(/grid-column\s*:\s*1\s*\/\s*-1\s*;/);
expect(findRule(tabletCss, ".planning-plan-actions .btn.planning-add-comment svg")).toMatch(/width\s*:\s*var\(--space-lg\)\s*;/);
});
it("shows exactly one contextual comment trigger in the tablet/phone plan action rail", () => {
it("shows exactly one contextual comment trigger in the plan action rail at every breakpoint", () => {
const css = loadPlanningCss();
const compactCss = getMediaBlocks(css, "@media (max-width: 1024px)").join("\n");
const mobileCss = getMediaBlocks(css, MOBILE_ACTIONS_QUERY).join("\n");
const railTriggerRule = findRule(compactCss, ".planning-plan-actions .btn.planning-add-comment--mobile");
const railTriggerRule = findRule(compactCss, ".planning-plan-actions .btn.planning-add-comment");
const mobileEditorRule = findRule(mobileCss, ".planning-comment-editor");
expect(findRule(css, ".planning-add-comment--mobile")).toMatch(/display\s*:\s*none\s*;/);
expect(findRule(compactCss, ".planning-add-comment--document")).toMatch(/display\s*:\s*none\s*;/);
/*
FNXC:PlanningComments 2026-07-25-10:20:
The --document / --mobile variant pair is deleted: both rendered, so desktop showed a duplicate
Add-comment button at the end of the plan text. One rail trigger, never display:none'd.
*/
expect(css).not.toMatch(/planning-add-comment--(document|mobile)/);
expect(findRule(css, ".planning-add-comment")).toMatch(/display\s*:\s*inline-flex\s*;/);
expect(findRule(css, ".planning-add-comment")).not.toMatch(/display\s*:\s*none\s*;/);
expect(railTriggerRule).toMatch(/display\s*:\s*flex\s*;/);
expect(railTriggerRule).toMatch(/grid-column\s*:\s*1\s*\/\s*-1\s*;/);
expect(railTriggerRule).toMatch(/margin-top\s*:\s*0\s*;/);
expect(findRule(compactCss, ".planning-plan-actions .btn.planning-add-comment--mobile svg")).toMatch(/width\s*:\s*var\(--space-lg\)\s*;/);
expect(findRule(compactCss, ".planning-plan-actions .btn.planning-add-comment svg")).toMatch(/width\s*:\s*var\(--space-lg\)\s*;/);
// FNXC:PlanningComments 2026-07-24-05:50: phone no longer overrides the rail trigger to fixed.
expect(findRule(mobileCss, ".planning-plan-actions .btn.planning-add-comment--mobile")).toBeUndefined();
expect(findRule(mobileCss, ".planning-plan-actions .btn.planning-add-comment")).toBeUndefined();
// FNXC:PlanningComments 2026-07-24-06:05: tablet+phone pin the composer; phone clears nav when keyboard closed.
expect(findRule(compactCss, ".planning-comment-editor")).toMatch(/position\s*:\s*fixed\s*;/);
expect(findRule(mobileCss, ".planning-comment-editor:not(.planning-comment-editor--keyboard-open)")).toMatch(/var\(--mobile-nav-height/);

View File

@@ -343,12 +343,15 @@ describe("PlanningModeModal sequential flow", () => {
};
selectQuote("Build authentication system");
const actionBar = screen.getByTestId("planning-plan-actions");
const documentTrigger = document.querySelector(".planning-add-comment--document");
const mobileTrigger = document.querySelector(".planning-add-comment--mobile");
expect(documentTrigger).toBeInstanceOf(HTMLElement);
expect(mobileTrigger).toBeInstanceOf(HTMLElement);
expect(documentTrigger?.closest(".planning-plan-document")).toContainElement(documentTrigger as HTMLElement);
expect(actionBar).toContainElement(mobileTrigger as HTMLElement);
/*
FNXC:PlanningComments 2026-07-25-10:20:
Exactly one trigger, and it lives in the plan action rail — never inside the plan document,
where it duplicated the rail control at the end of the plan text.
*/
const triggers = document.querySelectorAll(".planning-add-comment");
expect(triggers).toHaveLength(1);
expect(actionBar).toContainElement(triggers[0] as HTMLElement);
expect(document.querySelector(".planning-plan-document .planning-add-comment")).toBeNull();
const openTrigger = screen.getByRole("button", { name: "Add comment to selection" });
fireEvent.pointerDown(openTrigger);
// FNXC:PlanningComments 2026-07-24-06:30: selection collapse before click must not drop the frozen open quote.
@@ -424,6 +427,45 @@ describe("PlanningModeModal sequential flow", () => {
await waitFor(() => expect(screen.queryByRole("button", { name: "Add comment to selection" })).toBeNull());
});
/*
FNXC:PlanningComments 2026-07-25-10:20:
Desktop drag-select emits selectionchange per mouse move. Showing the trigger on those intermediate
ranges strobed the button while the operator was still dragging; it must appear once, on release.
*/
it("shows the selection comment trigger only after the drag selection is finished", async () => {
mockFetchAiSession.mockResolvedValue({
...base,
status: "awaiting_input",
currentQuestion: JSON.stringify({ id: "q-1", type: "text", question: "Anything else?" }),
result: JSON.stringify(summaryWithRefinements),
inputPayload: "{}",
});
renderSession();
const documentNode = await screen.findByTestId("planning-plan-markdown");
const walker = document.createTreeWalker(documentNode, NodeFilter.SHOW_TEXT);
let textNode: Node | null = walker.nextNode();
while (textNode && !textNode.textContent?.includes("Build authentication system")) textNode = walker.nextNode();
expect(textNode).not.toBeNull();
act(() => {
fireEvent.pointerDown(documentNode);
});
// Intermediate ranges during the drag must not mount the trigger.
act(() => {
const range = document.createRange();
range.selectNodeContents(textNode!);
window.getSelection()?.removeAllRanges();
window.getSelection()?.addRange(range);
document.dispatchEvent(new Event("selectionchange"));
});
expect(screen.queryByRole("button", { name: "Add comment to selection" })).toBeNull();
act(() => {
fireEvent.pointerUp(documentNode);
});
expect(await screen.findByRole("button", { name: "Add comment to selection" })).toBeInTheDocument();
});
it("rehydrates a restored idle session when another tab advances its question", async () => {
mockFetchAiSession.mockResolvedValue({
...base,

View File

@@ -20,9 +20,18 @@ describe("PlanningModeModal sequential layout", () => {
expect(component).toContain('document.addEventListener("selectionchange", capturePlanSelection)');
expect(component).toContain("selection.isCollapsed");
expect(component).toContain("Add comment to selection");
expect(component).toContain("planning-add-comment--document");
expect(component).toContain("planning-add-comment--mobile");
expect(component).toContain("mobileAddCommentTriggerRef");
/*
FNXC:PlanningComments 2026-07-25-10:20:
Exactly one Add-comment trigger, in the plan action rail. The --document / --mobile variant pair
rendered two buttons and must not come back.
*/
expect(component).not.toContain("planning-add-comment--document");
expect(component).not.toContain("planning-add-comment--mobile");
expect(component.match(/className="btn planning-add-comment"/g)).toHaveLength(1);
expect(component).toContain("addCommentTriggerRef");
// FNXC:PlanningComments 2026-07-25-10:20: the control appears once the drag-selection is done, not per selectionchange.
expect(component).toContain("planSelectionDragActiveRef");
expect(component).toContain('document.addEventListener("pointerup", handlePointerRelease)');
expect(component).toContain("contextualComments");
expect(component).toContain("setContextualComments([])");
// FNXC:PlanningComments 2026-07-24-06:20: prevent blur on pointerdown; commit on click.
@@ -42,10 +51,11 @@ describe("PlanningModeModal sequential layout", () => {
expect(css).toMatch(/@media \(max-width: 1024px\)[\s\S]*?\.planning-plan-actions\s*\{[^}]*display\s*:\s*grid\s*;[^}]*grid-template-columns\s*:\s*repeat\(2, minmax\(0, 1fr\)\)\s*;[^}]*gap\s*:\s*var\(--space-md\)\s*;[^}]*calc\(var\(--space-sm\) \+ env\(safe-area-inset-bottom\)\)/);
expect(css).toMatch(/@media \(max-width: 1024px\)[\s\S]*?\.planning-plan-actions \.btn\s*\{[^}]*width\s*:\s*100%\s*;/);
expect(css).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.planning-plan-actions\s*\{[^}]*gap\s*:\s*var\(--space-md\)\s*;[^}]*calc\(var\(--space-sm\) \+ env\(safe-area-inset-bottom\)\)/);
expect(css).toMatch(/\.planning-add-comment--mobile\s*\{[^}]*display\s*:\s*none\s*;/);
expect(css).toMatch(/@media \(max-width: 1024px\)[\s\S]*?\.planning-add-comment--document\s*\{[^}]*display\s*:\s*none\s*;/);
expect(css).toMatch(/@media \(max-width: 1024px\)[\s\S]*?\.planning-plan-actions \.btn\.planning-add-comment--mobile\s*\{[^}]*display\s*:\s*flex\s*;[^}]*grid-column\s*:\s*1\s*\/\s*-1\s*;/);
expect(css).not.toMatch(/@media \(max-width: 768px\)[\s\S]*?\.planning-plan-actions \.btn\.planning-add-comment--mobile\s*\{[^}]*position\s*:\s*fixed\s*;/);
// FNXC:PlanningComments 2026-07-25-10:20: one trigger everywhere — no breakpoint hides or duplicates it.
expect(css).not.toMatch(/planning-add-comment--(document|mobile)/);
expect(css).toMatch(/\.planning-add-comment\s*\{[^}]*display\s*:\s*inline-flex\s*;/);
expect(css).toMatch(/@media \(max-width: 1024px\)[\s\S]*?\.planning-plan-actions \.btn\.planning-add-comment\s*\{[^}]*display\s*:\s*flex\s*;[^}]*grid-column\s*:\s*1\s*\/\s*-1\s*;/);
expect(css).not.toMatch(/@media \(max-width: 768px\)[\s\S]*?\.planning-plan-actions \.btn\.planning-add-comment\s*\{[^}]*position\s*:\s*fixed\s*;/);
expect(css).toMatch(/@media \(max-width: 1024px\)[\s\S]*?\.planning-comment-editor\s*\{[^}]*position\s*:\s*fixed\s*;/);
});
});

View File

@@ -222,10 +222,15 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => {
async function verifyContextualCommentPlacement(
viewport: { width: number; height: number },
options: { inFooter: boolean; positionFixed: boolean },
options: { compact: boolean },
presentation: "embedded" | "modal",
): Promise<void> {
const { inFooter, positionFixed } = options;
/*
FNXC:PlanningComments 2026-07-25-10:20:
The trigger now lives in the plan action rail at EVERY viewport — the document-adjacent duplicate
that desktop used to render was removed. `compact` (<=1024) still selects the pinned composer.
*/
const { compact } = options;
const page = await browser.newPage({ viewport });
await page.goto(`${baseUrl}app/planning-browser-e2e-fixture.html?surface=plan-review&presentation=${presentation}&reset=1`);
if (viewport.width <= 768) await page.getByRole("tab", { name: "Plan preview" }).click();
@@ -277,25 +282,21 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => {
});
expect(placement).toMatchObject({
totalButtons: 2,
totalButtons: 1,
visibleButtons: 1,
visibleInActions: inFooter,
positionFixed,
// Compact shells keep the control in the visual viewport without scrolling the plan.
triggerInsideViewport: inFooter ? true : expect.any(Boolean),
visibleInActions: true,
positionFixed: false,
// The rail keeps the control in the visual viewport without scrolling the plan.
triggerInsideViewport: true,
triggerHasPreviousTabStop: true,
hiddenButtonsTabbable: 0,
});
if (inFooter) {
// In-flow rail row: Add comment sits above Refine/Proceed in the same footer.
expect(placement.actionLabels).toEqual(expect.arrayContaining([
"Add comment to selection",
"Refine",
"Proceed with plan",
]));
} else {
expect(placement.actionLabels).not.toContain("Add comment to selection");
}
// In-flow rail row: Add comment sits with Refine/Proceed in the same footer at every viewport.
expect(placement.actionLabels).toEqual(expect.arrayContaining([
"Add comment to selection",
"Refine",
"Proceed with plan",
]));
let reachedTriggerByTab = false;
for (let tabCount = 0; tabCount < 8; tabCount += 1) {
@@ -305,10 +306,10 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => {
}
expect(reachedTriggerByTab).toBe(true);
if (inFooter && presentation === "embedded" && process.env.FUSION_CAPTURE_DIR) await page.screenshot({ path: `${process.env.FUSION_CAPTURE_DIR}/planning-comment-mobile-selection.png` });
if (compact && presentation === "embedded" && process.env.FUSION_CAPTURE_DIR) await page.screenshot({ path: `${process.env.FUSION_CAPTURE_DIR}/planning-comment-mobile-selection.png` });
await page.getByRole("button", { name: "Add comment to selection" }).click();
await expectVisible(page.getByLabel("Add plan comment"));
if (inFooter && presentation === "embedded" && process.env.FUSION_CAPTURE_DIR) await page.screenshot({ path: `${process.env.FUSION_CAPTURE_DIR}/planning-comment-mobile-editor.png` });
if (compact && presentation === "embedded" && process.env.FUSION_CAPTURE_DIR) await page.screenshot({ path: `${process.env.FUSION_CAPTURE_DIR}/planning-comment-mobile-editor.png` });
const afterOpen = await page.evaluate(() => {
const editor = document.querySelector<HTMLElement>(".planning-comment-editor");
const editorRect = editor?.getBoundingClientRect();
@@ -329,8 +330,8 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => {
addCommentButtons: 0,
actionChildren: 0,
// Phone + tablet pin the composer; desktop keeps the in-document editor.
editorPosition: inFooter ? "fixed" : "static",
editorInsideViewport: inFooter ? true : expect.any(Boolean),
editorPosition: compact ? "fixed" : "static",
editorInsideViewport: compact ? true : expect.any(Boolean),
});
await page.evaluate(() => {
@@ -349,11 +350,11 @@ describe.runIf(executablePath)("Planning Mode browser E2E", () => {
it("places the sole contextual comment trigger by viewport in embedded and modal Planning", async () => {
for (const presentation of ["embedded", "modal"] as const) {
// Phone + tablet: full-width action-rail row above Refine/Proceed.
await verifyContextualCommentPlacement({ width: 768, height: 900 }, { inFooter: true, positionFixed: false }, presentation);
await verifyContextualCommentPlacement({ width: 769, height: 900 }, { inFooter: true, positionFixed: false }, presentation);
await verifyContextualCommentPlacement({ width: 1024, height: 900 }, { inFooter: true, positionFixed: false }, presentation);
// Desktop: document-adjacent trigger only.
await verifyContextualCommentPlacement({ width: 1280, height: 900 }, { inFooter: false, positionFixed: false }, presentation);
await verifyContextualCommentPlacement({ width: 768, height: 900 }, { compact: true }, presentation);
await verifyContextualCommentPlacement({ width: 769, height: 900 }, { compact: true }, presentation);
await verifyContextualCommentPlacement({ width: 1024, height: 900 }, { compact: true }, presentation);
// Desktop: same single rail trigger, inline with Refine/Proceed.
await verifyContextualCommentPlacement({ width: 1280, height: 900 }, { compact: false }, presentation);
}
}, 30_000);
});