feat(FN-1673): add editable AI suggestion drafts before acceptance
- Add editable drafts for AI-generated milestone and feature suggestions before acceptance - Users can edit suggestion title and description before creating the actual items - Draft edits persist when accepting individual or all suggestions - Add empty/whitespace title validation to prevent accepting blank suggestions - Add comprehensive tests for suggestion generation, editing, and acceptance - Fix prop type mismatch between RoadmapsView and MilestoneCard components - Add missing clearMilestoneSuggestions function to useRoadmaps hook - Update refs to synchronize immediately with state for proper test behavior - Add generateMilestoneSuggestions to API mock in component tests - Add documentation for the AI suggestion feature in dashboard-guide.md
This commit is contained in:
@@ -19,6 +19,7 @@ vi.mock("../../api", () => ({
|
||||
reorderRoadmapMilestones: vi.fn(),
|
||||
reorderRoadmapFeatures: vi.fn(),
|
||||
moveRoadmapFeature: vi.fn(),
|
||||
generateMilestoneSuggestions: vi.fn(),
|
||||
generateFeatureSuggestions: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -683,7 +684,7 @@ describe("useRoadmaps", () => {
|
||||
});
|
||||
|
||||
describe("Feature suggestions", () => {
|
||||
it("generates feature suggestions for a milestone", async () => {
|
||||
it("generates feature suggestions for a milestone with stable draft IDs", async () => {
|
||||
const mockSuggestions = [
|
||||
{ title: "Feature 1", description: "Description 1" },
|
||||
{ title: "Feature 2", description: "Description 2" },
|
||||
@@ -707,7 +708,14 @@ describe("useRoadmaps", () => {
|
||||
await result.current.generateFeatureSuggestions("RMS-001", { count: 5 });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.featureSuggestionsByMilestoneId["RMS-001"]).toEqual(mockSuggestions);
|
||||
const suggestions = result.current.featureSuggestionsByMilestoneId["RMS-001"];
|
||||
expect(suggestions).toHaveLength(2);
|
||||
expect(suggestions![0].title).toBe("Feature 1");
|
||||
expect(suggestions![1].title).toBe("Feature 2");
|
||||
// Verify stable draft IDs exist
|
||||
expect(suggestions![0].id).toBeDefined();
|
||||
expect(suggestions![1].id).toBeDefined();
|
||||
expect(suggestions![0].id).not.toBe(suggestions![1].id);
|
||||
});
|
||||
|
||||
expect(api.generateFeatureSuggestions).toHaveBeenCalledWith(
|
||||
@@ -737,12 +745,12 @@ describe("useRoadmaps", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts a feature suggestion", async () => {
|
||||
it("editing a draft changes the persisted value after accept-one", async () => {
|
||||
const mockFeature = {
|
||||
id: "RF-NEW",
|
||||
milestoneId: "RMS-001",
|
||||
title: "New Feature",
|
||||
description: "New description",
|
||||
title: "Edited Feature Title",
|
||||
description: "Edited description",
|
||||
orderIndex: 0,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
@@ -761,9 +769,9 @@ describe("useRoadmaps", () => {
|
||||
expect(result.current.selectedRoadmapId).toBe("RM-001");
|
||||
});
|
||||
|
||||
// Manually set suggestions (simulating what generateFeatureSuggestions would do)
|
||||
// Generate suggestions
|
||||
(api.generateFeatureSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
suggestions: [{ title: "New Feature", description: "New description" }],
|
||||
suggestions: [{ title: "Original Title", description: "Original description" }],
|
||||
});
|
||||
|
||||
await result.current.generateFeatureSuggestions("RMS-001");
|
||||
@@ -772,23 +780,39 @@ describe("useRoadmaps", () => {
|
||||
expect(result.current.featureSuggestionsByMilestoneId["RMS-001"]).toHaveLength(1);
|
||||
});
|
||||
|
||||
// Accept the suggestion
|
||||
await result.current.acceptFeatureSuggestion("RMS-001", 0);
|
||||
// Get the draft ID
|
||||
const draftId = result.current.featureSuggestionsByMilestoneId["RMS-001"][0].id;
|
||||
|
||||
// API should be called with the correct arguments
|
||||
// Edit the draft
|
||||
result.current.updateFeatureSuggestionDraft("RMS-001", draftId, {
|
||||
title: "Edited Feature Title",
|
||||
description: "Edited description",
|
||||
});
|
||||
|
||||
// Verify the draft is updated
|
||||
await waitFor(() => {
|
||||
const suggestion = result.current.featureSuggestionsByMilestoneId["RMS-001"][0];
|
||||
expect(suggestion.title).toBe("Edited Feature Title");
|
||||
expect(suggestion.description).toBe("Edited description");
|
||||
});
|
||||
|
||||
// Accept the suggestion - should use the edited values
|
||||
await result.current.acceptFeatureSuggestion("RMS-001", draftId);
|
||||
|
||||
// API should be called with the edited values
|
||||
expect(api.createRoadmapFeature).toHaveBeenCalledWith(
|
||||
"RMS-001",
|
||||
{ title: "New Feature", description: "New description" },
|
||||
{ title: "Edited Feature Title", description: "Edited description" },
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts all feature suggestions sequentially", async () => {
|
||||
it("mixed edited drafts persist in the same order on accept-all", async () => {
|
||||
const mockFeatures = [
|
||||
{
|
||||
id: "RF-NEW-1",
|
||||
milestoneId: "RMS-001",
|
||||
title: "Feature 1",
|
||||
title: "Edited Title 1",
|
||||
orderIndex: 0,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
@@ -796,7 +820,7 @@ describe("useRoadmaps", () => {
|
||||
{
|
||||
id: "RF-NEW-2",
|
||||
milestoneId: "RMS-001",
|
||||
title: "Feature 2",
|
||||
title: "Title 2",
|
||||
orderIndex: 1,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
@@ -821,8 +845,8 @@ describe("useRoadmaps", () => {
|
||||
// Set suggestions
|
||||
(api.generateFeatureSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
suggestions: [
|
||||
{ title: "Feature 1" },
|
||||
{ title: "Feature 2" },
|
||||
{ title: "Original Title 1" },
|
||||
{ title: "Original Title 2" },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -832,25 +856,31 @@ describe("useRoadmaps", () => {
|
||||
expect(result.current.featureSuggestionsByMilestoneId["RMS-001"]).toHaveLength(2);
|
||||
});
|
||||
|
||||
// Capture suggestions before accepting
|
||||
const suggestionsToAccept = [...(result.current.featureSuggestionsByMilestoneId["RMS-001"] || [])];
|
||||
expect(suggestionsToAccept).toHaveLength(2);
|
||||
// Get draft IDs
|
||||
const suggestions = result.current.featureSuggestionsByMilestoneId["RMS-001"];
|
||||
const draftId1 = suggestions[0].id;
|
||||
const draftId2 = suggestions[1].id;
|
||||
|
||||
// Edit the first draft
|
||||
result.current.updateFeatureSuggestionDraft("RMS-001", draftId1, {
|
||||
title: "Edited Title 1",
|
||||
});
|
||||
|
||||
// Accept all
|
||||
await result.current.acceptAllFeatureSuggestions("RMS-001");
|
||||
|
||||
// Verify sequential calls (not parallel)
|
||||
// Verify sequential calls with edited value for first suggestion
|
||||
expect(api.createRoadmapFeature).toHaveBeenCalledTimes(2);
|
||||
expect(api.createRoadmapFeature).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"RMS-001",
|
||||
{ title: "Feature 1", description: undefined },
|
||||
{ title: "Edited Title 1", description: undefined },
|
||||
undefined
|
||||
);
|
||||
expect(api.createRoadmapFeature).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"RMS-001",
|
||||
{ title: "Feature 2", description: undefined },
|
||||
{ title: "Original Title 2", description: undefined },
|
||||
undefined
|
||||
);
|
||||
});
|
||||
@@ -989,5 +1019,446 @@ describe("useRoadmaps", () => {
|
||||
// Suggestions should be cleared
|
||||
expect(result.current.featureSuggestionsByMilestoneId["RMS-001"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stale async suggestion responses are ignored after project change", async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ projectId }: { projectId?: string }) => useRoadmaps({ projectId }),
|
||||
{ initialProps: { projectId: "proj-1" } }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
result.current.selectRoadmap("RM-001");
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedRoadmapId).toBe("RM-001");
|
||||
});
|
||||
|
||||
// Set up a slow-responding mock
|
||||
let resolveGenerate: (value: { suggestions: Array<{ title: string }> }) => void;
|
||||
(api.generateFeatureSuggestions as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
||||
return new Promise((resolve) => {
|
||||
resolveGenerate = resolve;
|
||||
});
|
||||
});
|
||||
|
||||
// Start generating
|
||||
const generatePromise = result.current.generateFeatureSuggestions("RMS-001");
|
||||
|
||||
// Change project before the promise resolves
|
||||
rerender({ projectId: "proj-2" });
|
||||
|
||||
// Resolve the promise - should be ignored
|
||||
resolveGenerate!({ suggestions: [{ title: "Stale Feature" }] });
|
||||
await generatePromise;
|
||||
|
||||
// Suggestions should NOT be set for the old project
|
||||
// (Since we're now in project "proj-2", the stale response should be ignored)
|
||||
expect(result.current.featureSuggestionsByMilestoneId["RMS-001"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Milestone suggestions", () => {
|
||||
it("generates milestone suggestions with stable draft IDs", async () => {
|
||||
const mockSuggestions = [
|
||||
{ title: "Milestone 1", description: "Description 1" },
|
||||
{ title: "Milestone 2", description: "Description 2" },
|
||||
];
|
||||
|
||||
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
suggestions: mockSuggestions,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRoadmaps());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
result.current.selectRoadmap("RM-001");
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedRoadmapId).toBe("RM-001");
|
||||
});
|
||||
|
||||
await result.current.generateMilestoneSuggestions("Build an app", 5);
|
||||
|
||||
await waitFor(() => {
|
||||
const suggestions = result.current.milestoneSuggestions;
|
||||
expect(suggestions).toHaveLength(2);
|
||||
expect(suggestions[0].title).toBe("Milestone 1");
|
||||
expect(suggestions[1].title).toBe("Milestone 2");
|
||||
// Verify stable draft IDs exist
|
||||
expect(suggestions[0].id).toBeDefined();
|
||||
expect(suggestions[1].id).toBeDefined();
|
||||
expect(suggestions[0].id).not.toBe(suggestions[1].id);
|
||||
});
|
||||
});
|
||||
|
||||
it("editing a draft changes the persisted value after accept-one", async () => {
|
||||
const mockMilestone = {
|
||||
id: "RMS-NEW",
|
||||
roadmapId: "RM-001",
|
||||
title: "Edited Milestone Title",
|
||||
description: "Edited description",
|
||||
orderIndex: 0,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
(api.createRoadmapMilestone as ReturnType<typeof vi.fn>).mockResolvedValue(mockMilestone);
|
||||
|
||||
const { result } = renderHook(() => useRoadmaps());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
result.current.selectRoadmap("RM-001");
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedRoadmapId).toBe("RM-001");
|
||||
});
|
||||
|
||||
// Generate suggestions
|
||||
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
suggestions: [{ title: "Original Title", description: "Original description" }],
|
||||
});
|
||||
|
||||
await result.current.generateMilestoneSuggestions("Build something", 5);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.milestoneSuggestions).toHaveLength(1);
|
||||
});
|
||||
|
||||
// Get the draft ID
|
||||
const draftId = result.current.milestoneSuggestions[0].id;
|
||||
|
||||
// Edit the draft
|
||||
result.current.updateMilestoneSuggestionDraft(draftId, {
|
||||
title: "Edited Milestone Title",
|
||||
description: "Edited description",
|
||||
});
|
||||
|
||||
// Verify the draft is updated
|
||||
await waitFor(() => {
|
||||
const suggestion = result.current.milestoneSuggestions[0];
|
||||
expect(suggestion.title).toBe("Edited Milestone Title");
|
||||
expect(suggestion.description).toBe("Edited description");
|
||||
});
|
||||
|
||||
// Accept the suggestion - should use the edited values
|
||||
await result.current.acceptMilestoneSuggestion(draftId);
|
||||
|
||||
// API should be called with the edited values
|
||||
expect(api.createRoadmapMilestone).toHaveBeenCalledWith(
|
||||
"RM-001",
|
||||
{ title: "Edited Milestone Title", description: "Edited description" },
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it("mixed edited drafts persist in the same order on accept-all", async () => {
|
||||
const mockMilestones = [
|
||||
{
|
||||
id: "RMS-NEW-1",
|
||||
roadmapId: "RM-001",
|
||||
title: "Edited Title 1",
|
||||
orderIndex: 0,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "RMS-NEW-2",
|
||||
roadmapId: "RM-001",
|
||||
title: "Title 2",
|
||||
orderIndex: 1,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
(api.createRoadmapMilestone as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce(mockMilestones[0])
|
||||
.mockResolvedValueOnce(mockMilestones[1]);
|
||||
|
||||
const { result } = renderHook(() => useRoadmaps());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
result.current.selectRoadmap("RM-001");
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedRoadmapId).toBe("RM-001");
|
||||
});
|
||||
|
||||
// Set suggestions
|
||||
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
suggestions: [
|
||||
{ title: "Original Title 1" },
|
||||
{ title: "Original Title 2" },
|
||||
],
|
||||
});
|
||||
|
||||
await result.current.generateMilestoneSuggestions("Build something", 5);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.milestoneSuggestions).toHaveLength(2);
|
||||
});
|
||||
|
||||
// Get draft IDs
|
||||
const suggestions = result.current.milestoneSuggestions;
|
||||
const draftId1 = suggestions[0].id;
|
||||
const draftId2 = suggestions[1].id;
|
||||
|
||||
// Edit the first draft
|
||||
result.current.updateMilestoneSuggestionDraft(draftId1, {
|
||||
title: "Edited Title 1",
|
||||
});
|
||||
|
||||
// Accept all
|
||||
await result.current.acceptAllMilestoneSuggestions();
|
||||
|
||||
// Verify sequential calls with edited value for first suggestion
|
||||
expect(api.createRoadmapMilestone).toHaveBeenCalledTimes(2);
|
||||
expect(api.createRoadmapMilestone).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"RM-001",
|
||||
{ title: "Edited Title 1", description: undefined },
|
||||
undefined
|
||||
);
|
||||
expect(api.createRoadmapMilestone).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"RM-001",
|
||||
{ title: "Original Title 2", description: undefined },
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it("clearing drafts removes only draft state (not already persisted milestones)", async () => {
|
||||
const { result } = renderHook(() => useRoadmaps());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
result.current.selectRoadmap("RM-001");
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedRoadmapId).toBe("RM-001");
|
||||
});
|
||||
|
||||
// Set suggestions
|
||||
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
suggestions: [{ title: "Suggestion 1" }, { title: "Suggestion 2" }],
|
||||
});
|
||||
|
||||
await result.current.generateMilestoneSuggestions("Build something", 5);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.milestoneSuggestions).toHaveLength(2);
|
||||
expect(result.current.milestones).toHaveLength(2); // Existing milestones from mock
|
||||
});
|
||||
|
||||
// Clear suggestions
|
||||
result.current.clearMilestoneSuggestions();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.milestoneSuggestions).toHaveLength(0);
|
||||
// Existing milestones should still be there
|
||||
expect(result.current.milestones).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("stale async suggestion responses are ignored after project change", async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ projectId }: { projectId?: string }) => useRoadmaps({ projectId }),
|
||||
{ initialProps: { projectId: "proj-1" } }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
result.current.selectRoadmap("RM-001");
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedRoadmapId).toBe("RM-001");
|
||||
});
|
||||
|
||||
// Set up a slow-responding mock
|
||||
let resolveGenerate: (value: { suggestions: Array<{ title: string }> }) => void;
|
||||
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
||||
return new Promise((resolve) => {
|
||||
resolveGenerate = resolve;
|
||||
});
|
||||
});
|
||||
|
||||
// Start generating
|
||||
const generatePromise = result.current.generateMilestoneSuggestions("Build something", 5);
|
||||
|
||||
// Change project before the promise resolves
|
||||
rerender({ projectId: "proj-2" });
|
||||
|
||||
// Resolve the promise - should be ignored
|
||||
resolveGenerate!({ suggestions: [{ title: "Stale Milestone" }] });
|
||||
await generatePromise;
|
||||
|
||||
// Suggestions should NOT be set for the old project
|
||||
expect(result.current.milestoneSuggestions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("prevents acceptance of empty title", async () => {
|
||||
const { result } = renderHook(() => useRoadmaps());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
result.current.selectRoadmap("RM-001");
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedRoadmapId).toBe("RM-001");
|
||||
});
|
||||
|
||||
// Generate suggestions
|
||||
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
suggestions: [{ title: "Valid Title" }],
|
||||
});
|
||||
|
||||
await result.current.generateMilestoneSuggestions("Build something", 5);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.milestoneSuggestions).toHaveLength(1);
|
||||
});
|
||||
|
||||
const draftId = result.current.milestoneSuggestions[0].id;
|
||||
|
||||
// Edit to make title empty
|
||||
result.current.updateMilestoneSuggestionDraft(draftId, {
|
||||
title: "",
|
||||
});
|
||||
|
||||
// Try to accept - should fail
|
||||
const onError = vi.fn();
|
||||
await expect(
|
||||
result.current.acceptMilestoneSuggestion(draftId, { onError })
|
||||
).rejects.toThrow("Title cannot be empty");
|
||||
expect(onError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prevents acceptance of whitespace-only title", async () => {
|
||||
const { result } = renderHook(() => useRoadmaps());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
result.current.selectRoadmap("RM-001");
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedRoadmapId).toBe("RM-001");
|
||||
});
|
||||
|
||||
// Generate suggestions
|
||||
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
suggestions: [{ title: "Valid Title" }],
|
||||
});
|
||||
|
||||
await result.current.generateMilestoneSuggestions("Build something", 5);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.milestoneSuggestions).toHaveLength(1);
|
||||
});
|
||||
|
||||
const draftId = result.current.milestoneSuggestions[0].id;
|
||||
|
||||
// Edit to make title whitespace-only
|
||||
result.current.updateMilestoneSuggestionDraft(draftId, {
|
||||
title: " ",
|
||||
});
|
||||
|
||||
// Try to accept - should fail
|
||||
const onError = vi.fn();
|
||||
await expect(
|
||||
result.current.acceptMilestoneSuggestion(draftId, { onError })
|
||||
).rejects.toThrow("Title cannot be empty");
|
||||
expect(onError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts a single milestone suggestion by draftId", async () => {
|
||||
const mockMilestone = {
|
||||
id: "RMS-NEW",
|
||||
roadmapId: "RM-001",
|
||||
title: "New Milestone",
|
||||
orderIndex: 0,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
(api.createRoadmapMilestone as ReturnType<typeof vi.fn>).mockResolvedValue(mockMilestone);
|
||||
|
||||
const { result } = renderHook(() => useRoadmaps());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
result.current.selectRoadmap("RM-001");
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedRoadmapId).toBe("RM-001");
|
||||
});
|
||||
|
||||
// Generate suggestions
|
||||
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
suggestions: [{ title: "New Milestone", description: "Description" }],
|
||||
});
|
||||
|
||||
await result.current.generateMilestoneSuggestions("Build something", 5);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.milestoneSuggestions).toHaveLength(1);
|
||||
});
|
||||
|
||||
const draftId = result.current.milestoneSuggestions[0].id;
|
||||
|
||||
// Accept the suggestion
|
||||
await result.current.acceptMilestoneSuggestion(draftId);
|
||||
|
||||
expect(api.createRoadmapMilestone).toHaveBeenCalledWith(
|
||||
"RM-001",
|
||||
{ title: "New Milestone", description: "Description" },
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it("clears milestone suggestions when project changes", async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ projectId }: { projectId?: string }) => useRoadmaps({ projectId }),
|
||||
{ initialProps: { projectId: "proj-1" } }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
result.current.selectRoadmap("RM-001");
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedRoadmapId).toBe("RM-001");
|
||||
});
|
||||
|
||||
// Set suggestions
|
||||
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
suggestions: [{ title: "Milestone" }],
|
||||
});
|
||||
|
||||
await result.current.generateMilestoneSuggestions("Build something", 5);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.milestoneSuggestions).toHaveLength(1);
|
||||
});
|
||||
|
||||
// Change project
|
||||
rerender({ projectId: "proj-2" });
|
||||
|
||||
// Suggestions should be cleared
|
||||
expect(result.current.milestoneSuggestions).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user