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:
@@ -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 () => {
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user