FN-8245: stabilize dashboard focus and planning tests
Make dashboard test execution deterministic and restore quarantined coverage. - Defer oversight-menu autofocus until the opening frame and cover both breakpoints. - Replace timing-dependent planning stream mocks with deterministic microtasks. - Isolate QuickEntryBox focus state and re-admit restored dashboard tests. Files changed: .../dashboard/app/components/TaskDetailModal.tsx | 15 ++- .../PlanningModeModal.planning-flow.test.tsx | 132 ++++++++++++++------- .../components/__tests__/QuickEntryBox.test.tsx | 13 ++ .../TaskDetailModal.oversight-mobile.test.tsx | 19 +-- packages/dashboard/vitest.config.ts | 24 ++-- scripts/lib/dashboard-curated-skiplist.json | 4 + scripts/lib/test-quarantine.json | 20 ---- 7 files changed, 140 insertions(+), 87 deletions(-) Fusion-Task-Id: FN-8245 Fusion-Task-Lineage: cd9b0638-0a6b-4dcf-980a-e90ba72b5db9 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -3713,14 +3713,23 @@ export function TaskDetailContent({
|
||||
firstMenuItem?.focus();
|
||||
}, [showMoveMenu]);
|
||||
|
||||
// FNXC:PlannerOversight 2026-07-04-00:00: FN-7562 — auto-focus the first actionable button menuitem, never the native oversight-level <select>; focusing the <select> surfaced its OS picker as a second menu overlapping the custom oversight popover on mobile.
|
||||
/*
|
||||
FNXC:PlannerOversight 2026-07-17-16:35:
|
||||
FN-8245 schedules oversight-menu autofocus after the opening commit, matching the
|
||||
sibling activity-view menu. The first actionable button (never the native level
|
||||
select) must receive focus at both breakpoints; synchronously focusing in the
|
||||
effect could lose the focus race while concurrent dashboard rendering settled.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!showOversightMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstMenuItem = oversightMenuRef.current?.querySelector<HTMLButtonElement>("button.detail-oversight-menu-item");
|
||||
firstMenuItem?.focus();
|
||||
const frame = requestAnimationFrame(() => {
|
||||
const firstMenuItem = oversightMenuRef.current?.querySelector<HTMLButtonElement>("button.detail-oversight-menu-item");
|
||||
firstMenuItem?.focus();
|
||||
});
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [showOversightMenu]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -66,6 +66,17 @@ import {
|
||||
const mockAddToast = vi.fn();
|
||||
const mockCopyTextToClipboard = vi.fn();
|
||||
|
||||
/*
|
||||
FNXC:PlanningModeStreamHarness 2026-07-17-16:20:
|
||||
FN-8245 requires planning-stream test events to retain their asynchronous protocol
|
||||
boundary without depending on wall-clock timers. Queue one deterministic microtask
|
||||
so every question, summary, and error settles before the awaiting assertion runs,
|
||||
even when dashboard tests share loaded jsdom workers.
|
||||
*/
|
||||
function queuePlanningStreamEvent(callback: () => void): void {
|
||||
queueMicrotask(callback);
|
||||
}
|
||||
|
||||
vi.mock("../../hooks/useToast", () => ({
|
||||
useOptionalToast: () => null,
|
||||
useToast: () => ({
|
||||
@@ -105,6 +116,29 @@ vi.mock("../../api", () => ({
|
||||
rejectPlan: (...args: any[]) => mockRejectPlan(...args),
|
||||
refineTask: (...args: any[]) => mockRefineTask(...args),
|
||||
fetchSettings: vi.fn().mockResolvedValue({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {} }),
|
||||
/*
|
||||
FNXC:PlanningModeSettings 2026-07-17-15:45:
|
||||
FN-8245 keeps the clarification-settings dependency deterministic for every
|
||||
planning interaction. The modal intentionally blocks Start Planning while
|
||||
settings load, so this test double settles synchronously rather than making
|
||||
each user-flow assertion depend on an arbitrary event-loop delay.
|
||||
*/
|
||||
fetchGlobalSettings: vi.fn(() => {
|
||||
const settled = {
|
||||
then(onFulfilled: (settings: Record<string, never>) => unknown) {
|
||||
onFulfilled({});
|
||||
return settled;
|
||||
},
|
||||
catch() {
|
||||
return settled;
|
||||
},
|
||||
finally(onFinally: () => unknown) {
|
||||
onFinally();
|
||||
return settled;
|
||||
},
|
||||
};
|
||||
return settled;
|
||||
}),
|
||||
fetchModels: (...args: any[]) => mockFetchModels(...args),
|
||||
fetchWorkflowSteps: vi.fn().mockResolvedValue([]),
|
||||
refineText: vi.fn(),
|
||||
@@ -188,12 +222,15 @@ describe("PlanningModeModal", () => {
|
||||
mockUpdatePlanningSessionDraft.mockResolvedValue({ ok: true });
|
||||
mockStopPlanningGeneration.mockResolvedValue({ success: true });
|
||||
|
||||
// Default: simulate receiving a question after a brief delay
|
||||
mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
handlers.onQuestion?.(mockQuestion);
|
||||
}, 10);
|
||||
|
||||
// Default stream behavior belongs only to fresh sessions. Resumed sessions restore their
|
||||
// persisted question and must not receive a synthetic fresh-session question.
|
||||
mockConnectPlanningStream.mockImplementation((sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
if (sessionId === "session-123") {
|
||||
queuePlanningStreamEvent(() => {
|
||||
handlers.onQuestion?.(mockQuestion);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
close: vi.fn(),
|
||||
isConnected: vi.fn().mockReturnValue(true),
|
||||
@@ -230,7 +267,7 @@ describe("PlanningModeModal", () => {
|
||||
it.each(["desktop", "mobile"] as const)("renders the mandatory deepening checkpoint before summary actions on %s", async (viewportMode) => {
|
||||
mockViewport(viewportMode);
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
handlers.onQuestion?.({
|
||||
id: PLANNING_DEEPEN_CHECKPOINT_ID,
|
||||
type: "multi_select",
|
||||
@@ -247,7 +284,7 @@ describe("PlanningModeModal", () => {
|
||||
keyDeliverables: ["Preview deliverable one", "Preview deliverable two"],
|
||||
},
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
return { close: vi.fn() };
|
||||
});
|
||||
|
||||
@@ -284,7 +321,7 @@ describe("PlanningModeModal", () => {
|
||||
let streamHandlers: any;
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
streamHandlers = handlers;
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
handlers.onQuestion?.({
|
||||
id: PLANNING_DEEPEN_CHECKPOINT_ID,
|
||||
type: "multi_select",
|
||||
@@ -294,7 +331,7 @@ describe("PlanningModeModal", () => {
|
||||
{ id: "theme-ux", label: "UX and interaction details" },
|
||||
],
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
return { close: vi.fn() };
|
||||
});
|
||||
mockRespondToPlanning.mockResolvedValue({ type: "question", data: null });
|
||||
@@ -379,13 +416,13 @@ describe("PlanningModeModal", () => {
|
||||
mockViewport(viewportMode);
|
||||
mockStartPlanningStreaming.mockResolvedValueOnce({ sessionId: `session-fn-6977-live-${viewportMode}` });
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
handlers.onSummary?.({
|
||||
title: "Live malformed summary",
|
||||
description: "Live Planning Mode summary omitted deliverable arrays",
|
||||
suggestedSize: "M",
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
|
||||
return {
|
||||
close: vi.fn(),
|
||||
@@ -412,7 +449,7 @@ describe("PlanningModeModal", () => {
|
||||
expect(screen.getByText("Planning Complete!")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.getByDisplayValue("Live Planning Mode summary omitted deliverable arrays")).toBeDefined();
|
||||
expect(screen.getByText("Live Planning Mode summary omitted deliverable arrays")).toBeDefined();
|
||||
expect(screen.queryByText(/Something went wrong/i)).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Create Single Task" })).toBeEnabled();
|
||||
expect(screen.getByRole("button", { name: "Break into Tasks" })).toBeEnabled();
|
||||
@@ -439,6 +476,7 @@ describe("PlanningModeModal", () => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, undefined, {
|
||||
planningDepth: "medium",
|
||||
customQuestionCount: undefined,
|
||||
clarificationEnabled: false,
|
||||
}, undefined);
|
||||
});
|
||||
|
||||
@@ -625,7 +663,7 @@ describe("PlanningModeModal", () => {
|
||||
it("allows Other-only answers for multi-select planning questions", async () => {
|
||||
window.sessionStorage.setItem("fusion-tab-id", "tab-self");
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
handlers.onQuestion?.({
|
||||
id: "q-priorities",
|
||||
type: "multi_select",
|
||||
@@ -635,7 +673,7 @@ describe("PlanningModeModal", () => {
|
||||
{ id: "quality", label: "Quality" },
|
||||
],
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
return {
|
||||
close: vi.fn(),
|
||||
isConnected: vi.fn().mockReturnValue(true),
|
||||
@@ -665,7 +703,13 @@ describe("PlanningModeModal", () => {
|
||||
fireEvent.click(within(screen.getByTestId("planning-option-other")).getByRole("checkbox"));
|
||||
expect(continueButton).toBeDisabled();
|
||||
|
||||
const otherInput = screen.getByTestId("planning-other-input");
|
||||
/*
|
||||
FNXC:PlanningModeOptions 2026-07-17-15:55:
|
||||
FN-8245 waits for the Other input after its toggle commits React state.
|
||||
This preserves the user-visible invariant that Other opens an editable
|
||||
input without assuming a same-tick DOM update under loaded jsdom workers.
|
||||
*/
|
||||
const otherInput = await screen.findByTestId("planning-other-input");
|
||||
fireEvent.change(otherInput, { target: { value: " Challenge the premise " } });
|
||||
expect(continueButton).toBeEnabled();
|
||||
fireEvent.click(continueButton);
|
||||
@@ -683,7 +727,7 @@ describe("PlanningModeModal", () => {
|
||||
window.sessionStorage.setItem("fusion-tab-id", "tab-self");
|
||||
mockViewport("mobile");
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
handlers.onQuestion?.({
|
||||
id: "q-priorities",
|
||||
type: "multi_select",
|
||||
@@ -693,7 +737,7 @@ describe("PlanningModeModal", () => {
|
||||
{ id: "quality", label: "Quality" },
|
||||
],
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
return {
|
||||
close: vi.fn(),
|
||||
isConnected: vi.fn().mockReturnValue(true),
|
||||
@@ -740,14 +784,14 @@ describe("PlanningModeModal", () => {
|
||||
window.sessionStorage.setItem("fusion-tab-id", "tab-self");
|
||||
mockViewport(viewportMode);
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
handlers.onQuestion?.({
|
||||
id: "q-confirm-scope",
|
||||
type: "confirm",
|
||||
question: "Proceed with this scope?",
|
||||
description: "Choose Yes, No, or write a different answer.",
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
return {
|
||||
close: vi.fn(),
|
||||
isConnected: vi.fn().mockReturnValue(true),
|
||||
@@ -804,13 +848,13 @@ describe("PlanningModeModal", () => {
|
||||
it("clears confirm Other text when switching back to Yes or No", async () => {
|
||||
window.sessionStorage.setItem("fusion-tab-id", "tab-self");
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
handlers.onQuestion?.({
|
||||
id: "q-confirm-scope",
|
||||
type: "confirm",
|
||||
question: "Proceed with this scope?",
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
return {
|
||||
close: vi.fn(),
|
||||
isConnected: vi.fn().mockReturnValue(true),
|
||||
@@ -1266,7 +1310,7 @@ describe("PlanningModeModal", () => {
|
||||
mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
streamAttempt += 1;
|
||||
if (streamAttempt === 1) {
|
||||
setTimeout(() => handlers.onError?.("Connection lost"), 10);
|
||||
queuePlanningStreamEvent(() => handlers.onError?.("Connection lost"));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1320,7 +1364,7 @@ describe("PlanningModeModal", () => {
|
||||
mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
streamAttempt += 1;
|
||||
if (streamAttempt === 1) {
|
||||
setTimeout(() => handlers.onError?.("Connection lost"), 10);
|
||||
queuePlanningStreamEvent(() => handlers.onError?.("Connection lost"));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1761,7 +1805,7 @@ describe("PlanningModeModal", () => {
|
||||
expect(screen.getByText("Planning Complete!")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.getByDisplayValue("Recovered summary missing deliverable and dependency arrays")).toBeDefined();
|
||||
expect(screen.getByText("Recovered summary missing deliverable and dependency arrays")).toBeDefined();
|
||||
expect(screen.queryByText(/Something went wrong/i)).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Create Single Task" })).toBeEnabled();
|
||||
expect(screen.getByRole("button", { name: "Break into Tasks" })).toBeEnabled();
|
||||
@@ -2080,7 +2124,7 @@ describe("PlanningModeModal", () => {
|
||||
expect(screen.getByText("Planning Complete!")).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.getByDisplayValue("Recovered summary description from persisted session")).toBeDefined();
|
||||
expect(screen.getByText("Recovered summary description from persisted session")).toBeDefined();
|
||||
expect((screen.getByRole("combobox", { name: "Suggested Size" }) as HTMLSelectElement).value).toBe("L");
|
||||
expect(screen.getByText("Deliverable A")).toBeDefined();
|
||||
expect(screen.getByText("Deliverable B")).toBeDefined();
|
||||
@@ -2175,8 +2219,8 @@ describe("PlanningModeModal", () => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith(
|
||||
"Plan that needs a specific model",
|
||||
undefined,
|
||||
{ planningModelProvider: "anthropic", planningModelId: "claude-sonnet-4-5" },
|
||||
{ planningDepth: "medium", customQuestionCount: undefined },
|
||||
{ planningModelProvider: "anthropic", planningModelId: "claude-sonnet-4-5", thinkingLevel: undefined },
|
||||
{ planningDepth: "medium", customQuestionCount: undefined, clarificationEnabled: false },
|
||||
"session-draft-with-model",
|
||||
);
|
||||
});
|
||||
@@ -2270,7 +2314,7 @@ describe("PlanningModeModal", () => {
|
||||
expect(screen.getByText("Planning Complete!")).toBeDefined();
|
||||
});
|
||||
expect(screen.queryByPlaceholderText(/e.g., Build a user authentication/)).toBeNull();
|
||||
expect(screen.getByDisplayValue("Recovered summary from history")).toBeDefined();
|
||||
expect(screen.getByText("Recovered summary from history")).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Draft plan from history/i }));
|
||||
|
||||
@@ -3288,9 +3332,9 @@ describe("PlanningModeModal", () => {
|
||||
};
|
||||
});
|
||||
mockRespondToPlanning.mockImplementationOnce(async () => {
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
streamHandlers?.onQuestion?.(refinedQuestion);
|
||||
}, 10);
|
||||
});
|
||||
return { type: "question", data: refinedQuestion };
|
||||
});
|
||||
|
||||
@@ -3369,11 +3413,11 @@ describe("PlanningModeModal", () => {
|
||||
};
|
||||
});
|
||||
mockRespondToPlanning.mockImplementation(async () => {
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
if (!streamClosed) {
|
||||
streamHandlers?.onQuestion?.(refinedQuestion);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
return { type: "question", data: refinedQuestion };
|
||||
});
|
||||
|
||||
@@ -3451,11 +3495,11 @@ describe("PlanningModeModal", () => {
|
||||
};
|
||||
});
|
||||
mockRespondToPlanning.mockImplementationOnce(async () => {
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
if (!streamClosed) {
|
||||
streamHandlers?.onQuestion?.(refinedQuestion);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
throw new Error("Generation already in progress for this response");
|
||||
});
|
||||
|
||||
@@ -3723,9 +3767,9 @@ describe("PlanningModeModal", () => {
|
||||
let streamHandlers: any;
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
streamHandlers = handlers;
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
handlers.onQuestion?.(mockQuestion);
|
||||
}, 10);
|
||||
});
|
||||
|
||||
return {
|
||||
close: vi.fn(),
|
||||
@@ -3734,9 +3778,9 @@ describe("PlanningModeModal", () => {
|
||||
});
|
||||
|
||||
mockRespondToPlanning.mockImplementation(async () => {
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
streamHandlers?.onQuestion?.(secondQuestion);
|
||||
}, 10);
|
||||
});
|
||||
return { sessionId: "session-123", currentQuestion: null, summary: null };
|
||||
});
|
||||
|
||||
@@ -3802,9 +3846,9 @@ describe("PlanningModeModal", () => {
|
||||
let streamHandlers: any;
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
streamHandlers = handlers;
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
handlers.onQuestion?.(mockQuestion);
|
||||
}, 10);
|
||||
});
|
||||
|
||||
return {
|
||||
close: vi.fn(),
|
||||
@@ -3816,9 +3860,9 @@ describe("PlanningModeModal", () => {
|
||||
mockRespondToPlanning.mockImplementation(async () => {
|
||||
respondCallCount += 1;
|
||||
const nextQuestion = respondCallCount === 1 ? secondQuestion : thirdQuestion;
|
||||
setTimeout(() => {
|
||||
queuePlanningStreamEvent(() => {
|
||||
streamHandlers?.onQuestion?.(nextQuestion);
|
||||
}, 10);
|
||||
});
|
||||
return { sessionId: "session-123", currentQuestion: null, summary: null };
|
||||
});
|
||||
|
||||
@@ -3893,7 +3937,7 @@ describe("PlanningModeModal", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "What are the key requirements?" })).toBeDefined();
|
||||
expect(screen.getByText("What are the key requirements?")).toBeDefined();
|
||||
});
|
||||
|
||||
// Symptom assertion (FN-7615): after the async rewind settles, the generation view must
|
||||
|
||||
@@ -482,6 +482,16 @@ function expectQuickEntryPrimaryIconCluster() {
|
||||
describe("QuickEntryBox", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
/*
|
||||
FNXC:QuickEntryFocus 2026-07-17-15:15:
|
||||
FN-8245 found the fourth QuickEntryBox focus failure was cross-test jsdom
|
||||
focus leakage, not a component refocus. Clear any detached predecessor's
|
||||
active element before rendering so submit and action-button assertions start
|
||||
from the same browser focus baseline under loaded worker execution.
|
||||
*/
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
localStorage.clear();
|
||||
vi.mocked(fetchAgents).mockResolvedValue([]);
|
||||
@@ -530,6 +540,9 @@ describe("QuickEntryBox", () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
});
|
||||
vi.useRealTimers();
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
localStorage.clear();
|
||||
restoreQuickEntryTestGlobals();
|
||||
});
|
||||
|
||||
@@ -407,12 +407,13 @@ describe("TaskDetailModal oversight controls — mobile overflow menu", () => {
|
||||
(level-only) state.
|
||||
*/
|
||||
/*
|
||||
FNXC:DashboardTests 2026-07-16-12:25:
|
||||
Focus assertion can flake under concurrent quality load (activeElement never becomes nudge).
|
||||
Keep the test active so quarantine-ledger tooling can still list it; the suite is
|
||||
file-quarantined in vitest.config + test-quarantine.json rather than source-skipped.
|
||||
FNXC:PlannerOversight 2026-07-17-15:58:
|
||||
FN-8245 asserts the invariant against the actual first actionable menu item,
|
||||
including the session-advisor toggle that precedes Nudge. This replaces the
|
||||
stale Nudge-specific expectation without weakening the select-focus guard.
|
||||
*/
|
||||
it("auto-focuses the first button menuitem (never the native select) when nudge/stop/explain are available", async () => {
|
||||
it.each([["mobile", MOBILE_WIDTH], ["desktop", DESKTOP_WIDTH]] as const)("auto-focuses the first actionable button menuitem (never the native select) at %s width", async (_viewport, width) => {
|
||||
setViewportWidth(width);
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-213", column: "in-progress", plannerOversightLevel: "autonomous", plannerOverseerState: activeSnapshot })}
|
||||
@@ -429,10 +430,11 @@ describe("TaskDetailModal oversight controls — mobile overflow menu", () => {
|
||||
fireEvent.click(trigger);
|
||||
|
||||
const select = await screen.findByTestId("detail-oversight-level-select");
|
||||
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
|
||||
const firstAction = await screen.findByTestId("detail-session-advisor-toggle");
|
||||
await screen.findByTestId("detail-overseer-nudge");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.activeElement).toBe(nudgeBtn);
|
||||
expect(document.activeElement).toBe(firstAction);
|
||||
});
|
||||
expect(document.activeElement).not.toBe(select);
|
||||
|
||||
@@ -442,7 +444,8 @@ describe("TaskDetailModal oversight controls — mobile overflow menu", () => {
|
||||
expect(screen.getAllByRole("menu")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("does not fall back to focusing the native select when oversight is off and only the level control renders", async () => {
|
||||
it.each([["mobile", MOBILE_WIDTH], ["desktop", DESKTOP_WIDTH]] as const)("does not fall back to focusing the native select when oversight is off and only the level control renders at %s width", async (_viewport, width) => {
|
||||
setViewportWidth(width);
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-214", column: "todo", plannerOversightLevel: "off" })}
|
||||
|
||||
@@ -333,22 +333,19 @@ FN-8077 removed routes-system.test.ts from this list and the ledger in lockstep.
|
||||
*/
|
||||
const quarantinedDashboardTests: string[] = [
|
||||
/*
|
||||
FNXC:DashboardTests 2026-07-16-12:25:
|
||||
RuntimeFallbackBadge hang was a toast-context identity loop (PR #2229); component
|
||||
now depends on stable addToast. File re-admitted. Oversight-mobile focus flake
|
||||
quarantined on sight (ledger lockstep) instead of it.skip source skips.
|
||||
FNXC:DashboardTestQuarantine 2026-07-17-16:50:
|
||||
FN-8245 re-admits all three UI files with their ledger rows removed in lockstep.
|
||||
QuickEntryBox restores focus from its resolved submit path while isolated jsdom
|
||||
globals prevent cross-file focus leakage; PlanningModeModal stream doubles use
|
||||
deterministic microtasks instead of wall-clock timers; and the oversight menu
|
||||
focuses its first button after the opening frame, never the native select.
|
||||
*/
|
||||
"app/components/__tests__/TaskDetailModal.oversight-mobile.test.tsx",
|
||||
"app/components/__tests__/PlanningModeModal.planning-flow.test.tsx",
|
||||
"app/components/__tests__/QuickEntryBox.test.tsx",
|
||||
/*
|
||||
FNXC:DashboardTests 2026-07-17-22:10:
|
||||
FN-8240 verified the 18 VAL-REMOVAL-005 dashboard API tests on their PG-backed
|
||||
async-store or applicable mock/non-store contracts. Remove their ledger/exclude
|
||||
pairs so dashboard-api-quality-backfill collects the restored coverage.
|
||||
*/
|
||||
// FNXC:DashboardTests 2026-07-17-06:35: inventory + ledger lockstep — build-only dist assert not in quality projects.
|
||||
"src/__tests__/plugin-registry-dist.test.ts",
|
||||
];
|
||||
|
||||
const qualityApiTests = [
|
||||
@@ -395,9 +392,12 @@ const qualityAppBackfillTests = ["app/**/*.test.{ts,tsx}"];
|
||||
|
||||
const backfillApiExclude = [
|
||||
...qualityApiTests,
|
||||
// FNXC:DashboardDistArtifacts 2026-07-16-08:20: plugin-registry-dist asserts
|
||||
// emitted server files and runs through the explicit test:build command after
|
||||
// its dist bootstrap, rather than adding a full build to API backfill shards.
|
||||
/*
|
||||
FNXC:DashboardDistArtifacts 2026-07-17-15:10:
|
||||
FN-8245 reclassified plugin-registry-dist as a curated skip-list build-only
|
||||
assertion, not a flake. Keep this lane exclusion: test:build supplies its
|
||||
required emitted dist artifact without making every API backfill shard build.
|
||||
*/
|
||||
"src/__tests__/plugin-registry-dist.test.ts",
|
||||
...skipListDashboardGlobs.filter((file) => file.startsWith("src/")),
|
||||
];
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
{
|
||||
"file": "packages/dashboard/app/__tests__/build-output.test.ts",
|
||||
"reason": "asserts the built bundle; runs standalone via `pnpm --filter @fusion/dashboard test:build` (needs a prior vite build), not in the unit gate"
|
||||
},
|
||||
{
|
||||
"file": "packages/dashboard/src/__tests__/plugin-registry-dist.test.ts",
|
||||
"reason": "emitted-dist assertion; runs standalone via `pnpm --filter @fusion/dashboard test:build` after a build, not in the unit gate"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -110,26 +110,6 @@
|
||||
"file": "packages/engine/src/__tests__/reliability-interactions/meta-chain-auto-close.test.ts",
|
||||
"reason": "VAL-REMOVAL-005 PG migration: reliability fixture uses PG-backed store but sync APIs (getRunAuditEvents, getDatabase) fail in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.",
|
||||
"quarantinedAt": "2026-07-14"
|
||||
},
|
||||
{
|
||||
"file": "packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-mobile.test.tsx",
|
||||
"reason": "PR #2229 review: focus assertion flakes under concurrent quality load (activeElement never becomes the nudge menuitem). Quarantine on sight per deletion ratchet instead of it.skip in source. Mirrored exclude in packages/dashboard/vitest.config.ts. Rescue requires a deterministic focus harness (fakeTimers/userEvent), not timeout widening.",
|
||||
"quarantinedAt": "2026-07-16"
|
||||
},
|
||||
{
|
||||
"file": "packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx",
|
||||
"reason": "Flake under concurrent dashboard quality load. Quarantine on sight; mirrored packages/dashboard/vitest.config.ts. Full-suite inventory guard 2026-07-17.",
|
||||
"quarantinedAt": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"file": "packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx",
|
||||
"reason": "Flake under concurrent quality load. Quarantine on sight; mirrored packages/dashboard/vitest.config.ts. Full-suite inventory guard 2026-07-17.",
|
||||
"quarantinedAt": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"file": "packages/dashboard/src/__tests__/plugin-registry-dist.test.ts",
|
||||
"reason": "Ungated by quality projects while excluded from vitest runs. Mirrored packages/dashboard/vitest.config.ts. Full-suite inventory guard 2026-07-17.",
|
||||
"quarantinedAt": "2026-07-17"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user