FN-6743: rescue QuickEntryBox focus restoration

Restore reliable QuickEntryBox post-submit focus and return its test to the dashboard suite.

- Replace the ref-gated focus effect with a state-driven post-submit focus request.
- Cover desktop Enter, Save, duplicate-confirmed submit, mobile non-focus, and failed-submit draft preservation.
- Remove QuickEntryBox from dashboard quarantine while keeping the remaining workflow editor quarantine.

Files changed:
 .../dashboard/app/components/QuickEntryBox.tsx     | 46 +++++++++++++++++-----
 .../components/__tests__/QuickEntryBox.test.tsx    | 29 +++++++++++++-
 packages/dashboard/vitest.config.ts                |  5 ++-
 scripts/lib/test-quarantine.json                   |  5 ---
 4 files changed, 69 insertions(+), 16 deletions(-)

Fusion-Task-Id: FN-6743

Fusion-Task-Lineage: 2d458631-3ff3-4d96-8d91-bc2a17538be2
This commit is contained in:
gsxdsm
2026-06-19 17:49:06 -07:00
parent 5117944371
commit d06d1052db
4 changed files with 69 additions and 16 deletions

View File

@@ -97,6 +97,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
return "";
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [postSubmitFocusRequest, setPostSubmitFocusRequest] = useState(0);
// isExpanded controls textarea height styling (auto-resize)
const [isExpanded, setIsExpanded] = useState(true);
// isDisclosureExpanded controls visibility of the controls panel (Deps, Models, etc.)
@@ -106,7 +107,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
const fileInputRef = useRef<HTMLInputElement>(null);
const touchButtonRef = useRef<HTMLButtonElement | null>(null);
const justResetRef = useRef(false);
const justSubmittedRef = useRef(false);
const postSubmitFocusTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handledPostSubmitFocusRequestRef = useRef(0);
const previousProjectIdRef = useRef(projectId);
const [pendingImages, setPendingImages] = useState<PendingImage[]>([]);
const pendingImagesRef = useRef<PendingImage[]>([]);
@@ -321,21 +323,46 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
}
}, [description, isExpanded, autoResize]);
// Restore focus after an in-component submission completes (when textarea is re-enabled).
const requestFocusAfterSuccessfulSubmit = useCallback(() => {
setPostSubmitFocusRequest((request) => request + 1);
}, []);
/*
FNXC:QuickEntryFocus 2026-06-19-16:50:
Desktop users should keep typing after Enter, Save, or duplicate-confirmed task creation, while mobile users must not receive an automatic focus that opens the soft keyboard.
Drive the post-submit focus from a resolved-submit state request instead of a ref-gated effect so React state ordering cannot skip the restoration when the form clears under broad jsdom load.
*/
useEffect(() => {
if (!justSubmittedRef.current || isSubmitting || description !== "" || !textareaRef.current) {
if (
postSubmitFocusRequest === 0 ||
handledPostSubmitFocusRequestRef.current === postSubmitFocusRequest ||
isSubmitting ||
description !== "" ||
!textareaRef.current
) {
return;
}
justSubmittedRef.current = false;
// Use setTimeout to ensure focus happens after React re-enables the textarea.
const focusTimeout = setTimeout(() => {
handledPostSubmitFocusRequestRef.current = postSubmitFocusRequest;
if (postSubmitFocusTimeoutRef.current) {
clearTimeout(postSubmitFocusTimeoutRef.current);
}
postSubmitFocusTimeoutRef.current = setTimeout(() => {
postSubmitFocusTimeoutRef.current = null;
if (typeof window !== "undefined" && window.innerWidth > MOBILE_BREAKPOINT_PX) {
textareaRef.current?.focus();
}
}, 0);
return () => clearTimeout(focusTimeout);
}, [isSubmitting, description]);
return () => {
if (postSubmitFocusTimeoutRef.current) {
clearTimeout(postSubmitFocusTimeoutRef.current);
postSubmitFocusTimeoutRef.current = null;
}
};
}, [description, isSubmitting, postSubmitFocusRequest]);
// Clear dep search when dropdown closes
useEffect(() => {
@@ -542,7 +569,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
}
}
resetForm();
justSubmittedRef.current = true;
requestFocusAfterSuccessfulSubmit();
} catch (err) {
setDescription(originalDescription);
addToast(getErrorMessage(err) || t("tasks.createFailed", "Failed to create task"), "error");
@@ -574,6 +601,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
projectId,
addToast,
resetForm,
requestFocusAfterSuccessfulSubmit,
]);
const handleSubmit = useCallback(async () => {

View File

@@ -277,6 +277,10 @@ async function flushPendingTimers() {
});
}
async function waitForSubmitSuccessToClear(textarea: HTMLTextAreaElement) {
await waitFor(() => expect(textarea.value).toBe(""));
}
function openPriorityMenu() {
fireEvent.click(screen.getByTestId("quick-entry-priority-button"));
}
@@ -456,6 +460,7 @@ describe("QuickEntryBox", () => {
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => expect(onCreate).toHaveBeenCalledTimes(1));
await waitForSubmitSuccessToClear(textarea);
await flushPendingTimers();
expect(focusSpy).toHaveBeenCalledTimes(1);
@@ -473,6 +478,7 @@ describe("QuickEntryBox", () => {
clickSave();
await waitFor(() => expect(onCreate).toHaveBeenCalledTimes(1));
await waitForSubmitSuccessToClear(textarea);
await flushPendingTimers();
expect(focusSpy).toHaveBeenCalledTimes(1);
@@ -498,7 +504,7 @@ describe("QuickEntryBox", () => {
fireEvent.click(screen.getByRole("button", { name: "Create anyway" }));
await waitFor(() => expect(onCreate).toHaveBeenCalledTimes(1));
await waitFor(() => expect(textarea.value).toBe(""));
await waitForSubmitSuccessToClear(textarea);
await flushPendingTimers();
expect(focusSpy).toHaveBeenCalledTimes(1);
@@ -519,12 +525,33 @@ describe("QuickEntryBox", () => {
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => expect(onCreate).toHaveBeenCalledTimes(1));
await waitForSubmitSuccessToClear(textarea);
await flushPendingTimers();
expect(focusSpy).not.toHaveBeenCalled();
expect(document.activeElement).not.toBe(textarea);
});
it("preserves the draft without auto-focus when submission fails", async () => {
mockDesktopViewport();
const addToast = vi.fn();
const onCreate = vi.fn().mockRejectedValue(new Error("create failed"));
renderQuickEntryBox({ addToast, onCreate });
const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
const focusSpy = vi.spyOn(textarea, "focus");
fireEvent.change(textarea, { target: { value: "Failed task" } });
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => expect(onCreate).toHaveBeenCalledTimes(1));
await waitFor(() => expect(textarea.value).toBe("Failed task"));
await flushPendingTimers();
expect(addToast).toHaveBeenCalledWith("create failed", "error");
expect(focusSpy).not.toHaveBeenCalled();
expect(document.activeElement).not.toBe(textarea);
});
it("does not auto-focus after Escape clears a non-empty draft", async () => {
mockDesktopViewport();
renderQuickEntryBox({});

View File

@@ -282,9 +282,12 @@ Quarantine the concurrency-sensitive workflow editor file under the deletion rat
FNXC:DashboardTestQuarantine 2026-06-19-15:40:
FN-6742 rescued session-cross-tab before the 2026-07-03 deletion ratchet by reproducing ENOTEMPTY in dashboard-api-quality-backfill and fixing the test-owned route/close-callback teardown seam.
Keep it out of this exclude list so loaded API shards keep exercising cross-tab locking, beacon release, stale-lock expiry, SSE summaries, and stale-session cleanup.
FNXC:DashboardTestQuarantine 2026-06-19-16:50:
FN-6743 rescued QuickEntryBox's third quarantine cycle by replacing the ref-gated post-submit focus effect with a resolved-submit focus trigger and broadening desktop/mobile submit coverage.
Keep QuickEntryBox out of this exclude list so the dashboard app lanes exercise Enter, Save, duplicate-confirmed creation, mobile non-focus, and failure-preserves-draft focus invariants.
*/
const quarantinedDashboardTests: string[] = [
"app/components/__tests__/QuickEntryBox.test.tsx",
"app/components/__tests__/WorkflowNodeEditor.test.tsx",
];

View File

@@ -1,11 +1,6 @@
{
"$comment": "Flaky-test quarantine ledger (deletion ratchet — see AGENTS.md 'Flaky tests: quarantine on sight' and docs/testing.md 'Quarantine ledger and the deletion ratchet'). A test observed failing without a corresponding real bug is quarantined ON SIGHT: add an entry here AND a matching one-line `exclude` entry in that package's vitest config, in the same commit. Every entry needs a non-empty `reason` (link the failing run) and a `quarantinedAt` date — the entry expires 14 days later, at which point the test file is DELETED unless someone rescues it with evidence it catches real regressions plus a root-cause fix (never appeasement). There is deliberately no loader module and no automation around this file: it is a dated record, the vitest config exclude is the mechanism, and the sweep is policy executed by whoever touches the suite.",
"entries": [
{
"file": "packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx",
"reason": "FN-6697 local workspace `pnpm test` observed the post-submission focus restoration test fail in the broad dashboard app backfill shard, while a targeted rerun of QuickEntryBox with MailboxModal passed the QuickEntryBox assertions; quarantine the focus-timing flake instead of appeasing it while the terminal shortcut fix remains scoped.",
"quarantinedAt": "2026-06-19"
},
{
"file": "packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx",
"reason": "FN-6726 local workspace `pnpm test` observed the duplicate-merge-seam template conflict assertion fail only in the broad dashboard components-b shard, while a targeted rerun of that exact test passed; quarantine the workflow editor concurrency flake instead of appeasing unrelated template insertion behavior while the Command Center token containment fix remains scoped.",