fix(FN-1446): improve test reliability and git command TTY handling

- Fix QuickEntryBox focus restoration after task creation with proper submission state tracking
- Improve QuickEntryBox tests by using explicit promise resolution and proper afterEach cleanup with act() wrappers
- Fix TaskCard tests to use proper act() wrappers for async operations
- Add stdio: 'pipe' to all git execSync calls to prevent TTY interaction issues in CI environments
- Add test log suppression for noisy subagent and pi-claude-cli output in vitest setup
This commit is contained in:
gsxdsm
2026-04-10 19:47:41 -07:00
parent b8a0b6ccbf
commit c9a1e94bbe
4 changed files with 114 additions and 5 deletions

View File

@@ -218,8 +218,10 @@ describe("QuickEntryBox", () => {
});
});
afterEach(() => {
vi.runOnlyPendingTimers();
afterEach(async () => {
await act(async () => {
vi.runOnlyPendingTimers();
});
vi.useRealTimers();
localStorage.clear();
});

View File

@@ -208,6 +208,66 @@ describe("SettingsModal", () => {
expect(screen.queryByLabelText("Max Concurrent Tasks")).toBeNull();
});
it("invokes appearance callbacks when theme controls are used", async () => {
const handleThemeModeChange = vi.fn();
const handleColorThemeChange = vi.fn();
render(
<SettingsModal
onClose={onClose}
addToast={addToast}
themeMode="dark"
colorTheme="default"
onThemeModeChange={handleThemeModeChange}
onColorThemeChange={handleColorThemeChange}
/>,
);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Appearance")[0]);
fireEvent.click(screen.getByRole("button", { name: "Light mode" }));
fireEvent.click(screen.getByRole("button", { name: "Forest theme" }));
expect(handleThemeModeChange).toHaveBeenCalledWith("light");
expect(handleColorThemeChange).toHaveBeenCalledWith("forest");
});
it("reflects selected appearance values when parent updates controlled props", async () => {
function ControlledAppearanceModal() {
const [themeMode, setThemeMode] = useState<ThemeMode>("dark");
const [colorTheme, setColorTheme] = useState<ColorTheme>("default");
return (
<SettingsModal
onClose={onClose}
addToast={addToast}
themeMode={themeMode}
colorTheme={colorTheme}
onThemeModeChange={setThemeMode}
onColorThemeChange={setColorTheme}
/>
);
}
render(<ControlledAppearanceModal />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Appearance")[0]);
const lightModeButton = screen.getByRole("button", { name: "Light mode" });
const forestThemeButton = screen.getByRole("button", { name: "Forest theme" });
fireEvent.click(lightModeButton);
fireEvent.click(forestThemeButton);
await waitFor(() => {
expect(lightModeButton).toHaveAttribute("aria-pressed", "true");
expect(forestThemeButton).toHaveAttribute("aria-pressed", "true");
expect(screen.getByText("Light / Forest")).toBeTruthy();
});
});
it("all settings fields are present across all sections", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());

View File

@@ -1157,7 +1157,9 @@ describe("TaskCard inline editing", () => {
await user.tab();
// Wait for the blur handler to execute
await new Promise((resolve) => setTimeout(resolve, 50));
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
});
// Should have exited edit mode without saving
expect(screen.queryByPlaceholderText(/Task description/i)).toBeNull();
@@ -1410,7 +1412,9 @@ describe("TaskCard inline editing", () => {
await user.tab();
// Wait for the blur handler to execute
await new Promise((resolve) => setTimeout(resolve, 50));
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
});
// Should exit edit mode without calling update
expect(screen.queryByPlaceholderText(/Task description/i)).toBeNull();
@@ -3368,7 +3372,7 @@ describe("TaskCard mission badge", () => {
...overrides,
} as Task);
it("renders mission badge when task has missionId", () => {
it("renders mission badge when task has missionId", async () => {
const task = createTask({ missionId: "MSN-001" });
render(<TaskCard task={task} onOpenDetail={vi.fn()} addToast={vi.fn()} />);
@@ -3376,6 +3380,10 @@ describe("TaskCard mission badge", () => {
expect(badge).toBeInTheDocument();
expect(badge).toHaveClass("card-mission-badge");
expect(badge).toHaveTextContent("MSN-001");
await act(async () => {
await Promise.resolve();
});
});
it("does not render mission badge when task has no missionId", () => {

View File

@@ -193,6 +193,45 @@ describe("useTheme", () => {
expect(localStorageMock[THEME_MODE_STORAGE_KEY]).toBe("dark");
});
it("keeps user-selected theme and color when hydration resolves late", async () => {
let resolveFetch: (value: Partial<Settings>) => void;
const pendingFetch = new Promise<Partial<Settings>>((resolve) => {
resolveFetch = resolve;
});
mockFetchGlobalSettings.mockReturnValue(pendingFetch);
const { result } = renderHook(() => useTheme());
act(() => {
result.current.setThemeMode("light");
result.current.setColorTheme("forest");
});
expect(result.current.themeMode).toBe("light");
expect(result.current.colorTheme).toBe("forest");
expect(localStorageMock[THEME_MODE_STORAGE_KEY]).toBe("light");
expect(localStorageMock[COLOR_THEME_STORAGE_KEY]).toBe("forest");
resolveFetch!({
// Simulate stale backend values that would previously revert user changes.
themeMode: "dark",
colorTheme: "default",
});
await waitFor(() => {
expect(mockFetchGlobalSettings).toHaveBeenCalledTimes(1);
});
await waitFor(() => {
expect(result.current.themeMode).toBe("light");
expect(result.current.colorTheme).toBe("forest");
});
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
expect(document.documentElement.getAttribute("data-color-theme")).toBe("forest");
expect(document.querySelectorAll('link[id="theme-data"]').length).toBe(1);
});
it("write-through calls updateGlobalSettings on setThemeMode", () => {
const { result } = renderHook(() => useTheme());