Files
fusion/packages/dashboard/app/components/__tests__/RoadmapsView.test.tsx
Fusion a67feafccb feat(FN-1880): add mobile roadmap controls - list, create, navigate
- Add mobile-friendly roadmap view with list, create, and navigation controls
- Implement RoadmapCard and inline creation form for quick roadmap entry
- Add mobile-optimized navigation between roadmap views
- Include mobile touch targets and responsive layout using design tokens
- Add comprehensive tests for RoadmapsView mobile components
2026-04-16 03:56:09 -07:00

848 lines
28 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { RoadmapsView } from "../RoadmapsView";
import * as api from "../../api";
import type {
Roadmap,
RoadmapMilestone,
RoadmapFeature,
RoadmapWithHierarchy,
} from "@fusion/core";
// Mock the API module
vi.mock("../../api", () => ({
fetchRoadmaps: vi.fn(),
fetchRoadmap: vi.fn(),
createRoadmap: vi.fn(),
updateRoadmap: vi.fn(),
deleteRoadmap: vi.fn(),
createRoadmapMilestone: vi.fn(),
updateRoadmapMilestone: vi.fn(),
deleteRoadmapMilestone: vi.fn(),
createRoadmapFeature: vi.fn(),
updateRoadmapFeature: vi.fn(),
deleteRoadmapFeature: vi.fn(),
reorderRoadmapMilestones: vi.fn(),
reorderRoadmapFeatures: vi.fn(),
moveRoadmapFeature: vi.fn(),
generateFeatureSuggestions: vi.fn(),
generateMilestoneSuggestions: vi.fn(),
}));
// Mock lucide-react icons
vi.mock("lucide-react", () => ({
Plus: ({ size, ...props }: { size?: number }) => (
<svg data-testid="plus-icon" {...props}>{`Plus ${size || 16}`}</svg>
),
Pencil: (props: unknown) => <span data-testid="pencil-icon" {...props}>Edit</span>,
Trash2: (props: unknown) => <span data-testid="trash-icon" {...props}>Delete</span>,
Check: (props: unknown) => <span data-testid="check-icon" {...props}>Check</span>,
X: (props: unknown) => <span data-testid="x-icon" {...props}>X</span>,
GripVertical: (props: unknown) => <span data-testid="grip-icon" {...props}>Grip</span>,
Sparkles: (props: unknown) => <span data-testid="sparkles-icon" {...props}>Sparkles</span>,
Download: (props: unknown) => <span data-testid="download-icon" {...props}>Download</span>,
Copy: (props: unknown) => <span data-testid="copy-icon" {...props}>Copy</span>,
Loader: (props: unknown) => <span data-testid="loader-icon" {...props}>Loader</span>,
ArrowLeft: (props: unknown) => <span data-testid="arrow-left-icon" {...props}>ArrowLeft</span>,
ChevronLeft: (props: unknown) => <span data-testid="chevron-left-icon" {...props}>ChevronLeft</span>,
}));
// Viewport mode mock helper
function mockViewport(mode: "mobile" | "desktop") {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => {
const isMobileQuery = query === "(max-width: 768px)";
const isTabletQuery = query === "(min-width: 769px) and (max-width: 1024px)";
return {
matches: mode === "mobile" ? isMobileQuery : false,
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
};
}),
});
}
const mockRoadmaps: Roadmap[] = [
{
id: "RM-001",
title: "Q2 Roadmap",
description: "Q2 product roadmap",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
},
{
id: "RM-002",
title: "Q3 Roadmap",
description: "Q3 product roadmap",
createdAt: "2026-01-02T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
},
];
const mockRoadmapHierarchy: RoadmapWithHierarchy = {
id: "RM-001",
title: "Q2 Roadmap",
description: "Q2 product roadmap",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
milestones: [
{
id: "RMS-001",
roadmapId: "RM-001",
title: "Milestone 1",
description: "First milestone",
orderIndex: 0,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
features: [
{
id: "RF-001",
milestoneId: "RMS-001",
title: "Feature 1",
description: "First feature",
orderIndex: 0,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
},
],
},
{
id: "RMS-002",
roadmapId: "RM-001",
title: "Milestone 2",
description: "Second milestone",
orderIndex: 1,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
features: [],
},
],
};
const mockAddToast = vi.fn();
describe("RoadmapsView", () => {
beforeEach(() => {
vi.clearAllMocks();
mockViewport("desktop");
(api.fetchRoadmaps as ReturnType<typeof vi.fn>).mockResolvedValue(mockRoadmaps);
(api.fetchRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(mockRoadmapHierarchy);
vi.spyOn(window, "confirm").mockReturnValue(true);
});
afterEach(() => {
vi.restoreAllMocks();
});
it("renders roadmap list in sidebar", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
expect(screen.getByText("Q3 Roadmap")).toBeInTheDocument();
expect(screen.getByText("Roadmaps")).toBeInTheDocument();
});
it("shows empty state when no roadmaps exist", async () => {
(api.fetchRoadmaps as ReturnType<typeof vi.fn>).mockResolvedValue([]);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("No roadmaps yet. Click + to create one.")).toBeInTheDocument();
});
});
it("shows loading state initially", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
expect(screen.getByText("Loading roadmaps...")).toBeInTheDocument();
});
it("selects a roadmap and shows its content", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
expect(screen.getByTestId("edit-roadmap-btn")).toBeInTheDocument();
});
// Should show milestones
expect(screen.getByText("Milestone 1")).toBeInTheDocument();
expect(screen.getByText("Milestone 2")).toBeInTheDocument();
});
it("creates a new roadmap", async () => {
const newRoadmap = {
id: "RM-003",
title: "New Roadmap",
createdAt: "2026-01-03T00:00:00.000Z",
updatedAt: "2026-01-03T00:00:00.000Z",
};
(api.createRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(newRoadmap);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
// Click create button
const createBtn = screen.getByTestId("create-roadmap-btn");
fireEvent.click(createBtn);
// Fill in the form
const titleInput = screen.getByTestId("create-roadmap-title");
await userEvent.type(titleInput, "New Roadmap");
// Submit
const submitBtn = screen.getByTestId("create-roadmap-submit");
fireEvent.click(submitBtn);
await waitFor(() => {
expect(api.createRoadmap).toHaveBeenCalledWith(
{ title: "New Roadmap" },
undefined
);
expect(mockAddToast).toHaveBeenCalledWith("Roadmap created", "success");
});
});
it("edits a roadmap title inline", async () => {
(api.updateRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue({
...mockRoadmaps[0],
title: "Updated Title",
});
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
// Select the roadmap first
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
await waitFor(() => {
expect(screen.getByTestId("edit-roadmap-btn")).toBeInTheDocument();
});
// Click edit button
const editBtn = screen.getByTestId("edit-roadmap-btn");
fireEvent.click(editBtn);
// Should show input field
const titleInput = screen.getByTestId("roadmap-title-input");
expect(titleInput).toBeInTheDocument();
// Type new title
await userEvent.clear(titleInput);
await userEvent.type(titleInput, "Updated Title");
// Save
fireEvent.keyDown(titleInput, { key: "Enter" });
await waitFor(() => {
expect(api.updateRoadmap).toHaveBeenCalledWith("RM-001", { title: "Updated Title" }, undefined);
});
});
it("deletes a roadmap after confirmation", async () => {
(api.deleteRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
// Select the roadmap first
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
await waitFor(() => {
expect(screen.getByTestId("delete-roadmap-btn")).toBeInTheDocument();
});
// Click delete button
const deleteBtn = screen.getByTestId("delete-roadmap-btn");
fireEvent.click(deleteBtn);
await waitFor(() => {
expect(window.confirm).toHaveBeenCalled();
expect(api.deleteRoadmap).toHaveBeenCalledWith("RM-001", undefined);
expect(mockAddToast).toHaveBeenCalledWith("Roadmap deleted", "success");
});
});
it("cancels roadmap deletion when not confirmed", async () => {
vi.spyOn(window, "confirm").mockReturnValue(false);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
await waitFor(() => {
expect(screen.getByTestId("delete-roadmap-btn")).toBeInTheDocument();
});
const deleteBtn = screen.getByTestId("delete-roadmap-btn");
fireEvent.click(deleteBtn);
expect(api.deleteRoadmap).not.toHaveBeenCalled();
});
it("shows empty milestones state", async () => {
const emptyHierarchy: RoadmapWithHierarchy = {
...mockRoadmapHierarchy,
milestones: [],
};
(api.fetchRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(emptyHierarchy);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
await waitFor(() => {
expect(screen.getByText("This roadmap has no milestones.")).toBeInTheDocument();
});
});
it("shows empty features state for a milestone", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
await waitFor(() => {
expect(screen.getByText("Milestone 2")).toBeInTheDocument();
});
// Milestone 2 has no features
const milestone2 = screen.getByTestId("add-feature-RMS-002").closest(".roadmaps-view__milestone");
expect(milestone2?.textContent).toContain("No features yet.");
});
it("creates a milestone", async () => {
const newMilestone: RoadmapMilestone = {
id: "RMS-003",
roadmapId: "RM-001",
title: "New Milestone",
orderIndex: 2,
createdAt: "2026-01-03T00:00:00.000Z",
updatedAt: "2026-01-03T00:00:00.000Z",
};
(api.createRoadmapMilestone as ReturnType<typeof vi.fn>).mockResolvedValue(newMilestone);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
await waitFor(() => {
expect(screen.getByTestId("add-milestone-btn")).toBeInTheDocument();
});
// Click add milestone button
const addBtn = screen.getByTestId("add-milestone-btn");
fireEvent.click(addBtn);
// Fill in the form
const titleInput = screen.getByTestId("create-milestone-title");
await userEvent.type(titleInput, "New Milestone");
// Submit
const submitBtn = screen.getByTestId("create-milestone-submit");
fireEvent.click(submitBtn);
await waitFor(() => {
expect(api.createRoadmapMilestone).toHaveBeenCalledWith("RM-001", { title: "New Milestone" }, undefined);
expect(mockAddToast).toHaveBeenCalledWith("Milestone created", "success");
});
});
it("deletes a milestone after confirmation", async () => {
(api.deleteRoadmapMilestone as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
await waitFor(() => {
expect(screen.getByTestId("milestone-delete-RMS-001")).toBeInTheDocument();
});
const deleteBtn = screen.getByTestId("milestone-delete-RMS-001");
fireEvent.click(deleteBtn);
await waitFor(() => {
expect(window.confirm).toHaveBeenCalled();
expect(api.deleteRoadmapMilestone).toHaveBeenCalledWith("RMS-001", undefined);
expect(mockAddToast).toHaveBeenCalledWith("Milestone deleted", "success");
});
});
it("creates a feature in a milestone", async () => {
const newFeature: RoadmapFeature = {
id: "RF-002",
milestoneId: "RMS-001",
title: "New Feature",
orderIndex: 1,
createdAt: "2026-01-03T00:00:00.000Z",
updatedAt: "2026-01-03T00:00:00.000Z",
};
(api.createRoadmapFeature as ReturnType<typeof vi.fn>).mockResolvedValue(newFeature);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
await waitFor(() => {
expect(screen.getByTestId("add-feature-RMS-001")).toBeInTheDocument();
});
// Click add feature button
const addBtn = screen.getByTestId("add-feature-RMS-001");
fireEvent.click(addBtn);
// Fill in the form
const titleInput = screen.getByTestId("create-feature-title");
await userEvent.type(titleInput, "New Feature");
// Submit
const submitBtn = screen.getByTestId("create-feature-submit");
fireEvent.click(submitBtn);
await waitFor(() => {
expect(api.createRoadmapFeature).toHaveBeenCalledWith("RMS-001", { title: "New Feature" }, undefined);
expect(mockAddToast).toHaveBeenCalledWith("Feature created", "success");
});
});
it("deletes a feature after confirmation", async () => {
(api.deleteRoadmapFeature as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
await waitFor(() => {
expect(screen.getByTestId("feature-delete-RF-001")).toBeInTheDocument();
});
// Hover over feature to show actions
const featureItem = screen.getByTestId("feature-delete-RF-001").closest(".roadmaps-view__feature-item");
fireEvent.mouseEnter(featureItem!);
const deleteBtn = screen.getByTestId("feature-delete-RF-001");
fireEvent.click(deleteBtn);
await waitFor(() => {
expect(window.confirm).toHaveBeenCalled();
expect(api.deleteRoadmapFeature).toHaveBeenCalledWith("RF-001", undefined);
expect(mockAddToast).toHaveBeenCalledWith("Feature deleted", "success");
});
});
it("cancels inline edit on Escape", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
await waitFor(() => {
expect(screen.getByTestId("edit-roadmap-btn")).toBeInTheDocument();
});
// Start editing
const editBtn = screen.getByTestId("edit-roadmap-btn");
fireEvent.click(editBtn);
const titleInput = screen.getByTestId("roadmap-title-input");
expect(titleInput).toBeInTheDocument();
// Type something
await userEvent.type(titleInput, "Changed");
// Cancel with Escape
fireEvent.keyDown(titleInput, { key: "Escape" });
// Edit should be cancelled
await waitFor(() => {
expect(screen.queryByTestId("roadmap-title-input")).not.toBeInTheDocument();
});
expect(api.updateRoadmap).not.toHaveBeenCalled();
});
it("shows empty main state when no roadmap selected", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
expect(screen.getByText("Select a roadmap from the sidebar to view its milestones.")).toBeInTheDocument();
});
describe("Feature suggestions", () => {
it("shows AI Suggestions button when roadmap is selected", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
// Wait for roadmap to load
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
// Select roadmap
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
// Wait for milestone to load and button to appear
await waitFor(() => {
expect(screen.getByTestId("generate-features-RMS-001")).toBeInTheDocument();
}, { timeout: 3000 });
});
});
describe("Suggestion editing", () => {
it("can edit milestone suggestion before accepting", async () => {
// Mock milestone suggestion generation
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
suggestions: [
{ title: "Original Title", description: "Original description" },
],
});
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
// Select roadmap
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
// Wait for roadmap to load
await waitFor(() => {
expect(screen.getByText("Generate Milestone Ideas")).toBeInTheDocument();
});
// Generate suggestions
const goalInput = screen.getByTestId("goal-prompt-input");
await userEvent.type(goalInput, "Build an app");
const generateBtn = screen.getByTestId("generate-suggestions-btn");
fireEvent.click(generateBtn);
// Wait for suggestion to appear
await waitFor(() => {
expect(screen.getByText("Original Title")).toBeInTheDocument();
});
// This test verifies the suggestion appears after generation
expect(screen.getByText("Original Title")).toBeInTheDocument();
});
it("can edit feature suggestion before accepting", async () => {
// Mock feature suggestion generation
(api.generateFeatureSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
suggestions: [
{ title: "Feature Suggestion", description: "Feature description" },
],
});
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
// Select roadmap
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
// Wait for milestone to load
await waitFor(() => {
expect(screen.getByTestId("generate-features-RMS-001")).toBeInTheDocument();
});
// Generate feature suggestions
const suggestBtn = screen.getByTestId("generate-features-RMS-001");
fireEvent.click(suggestBtn);
// Wait for suggestion to appear
await waitFor(() => {
expect(screen.getByText("Feature Suggestion")).toBeInTheDocument();
});
// Verify the suggestion card is rendered
expect(screen.getByText("Feature Suggestion")).toBeInTheDocument();
expect(screen.getByText("Feature description")).toBeInTheDocument();
});
it("shows edit button on suggestion cards", async () => {
// Mock milestone suggestion generation
(api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
suggestions: [
{ title: "Test Milestone" },
],
});
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
});
// Select roadmap
const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
fireEvent.click(roadmapItem);
// Generate suggestions
const goalInput = screen.getByTestId("goal-prompt-input");
await userEvent.type(goalInput, "Build something");
const generateBtn = screen.getByTestId("generate-suggestions-btn");
fireEvent.click(generateBtn);
// Wait for suggestion to appear
await waitFor(() => {
expect(screen.getByText("Test Milestone")).toBeInTheDocument();
});
// Look for edit button - it should have data-testid
// The edit button is a pencil icon with testId like "suggestion-{id}-edit"
const editButtons = screen.queryAllByRole("button", { name: /edit/i });
expect(editButtons.length).toBeGreaterThan(0);
});
});
describe("Mobile roadmap controls", () => {
beforeEach(() => {
mockViewport("mobile");
});
afterEach(() => {
mockViewport("desktop");
});
it("shows mobile roadmap list when no roadmap selected on mobile", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-list")).toBeInTheDocument();
});
expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
expect(screen.getByText("Q3 Roadmap")).toBeInTheDocument();
expect(screen.getByTestId("mobile-create-roadmap-btn")).toBeInTheDocument();
});
it("shows mobile roadmap items when roadmaps exist on mobile", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
});
expect(screen.getByTestId("mobile-roadmap-item-RM-002")).toBeInTheDocument();
});
it("can select a roadmap from mobile list", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
});
// Click on a roadmap item
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
// Should show the mobile header with the roadmap title
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-header")).toBeInTheDocument();
expect(screen.getByText("Q2 Roadmap", { selector: ".roadmaps-view__mobile-header-title" })).toBeInTheDocument();
});
});
it("mobile back button deselects roadmap", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
});
// Select a roadmap
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-header")).toBeInTheDocument();
});
// Click back button
fireEvent.click(screen.getByTestId("mobile-back-btn"));
// Should show mobile list again
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-list")).toBeInTheDocument();
});
});
it("mobile create button shows create form", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-create-roadmap-btn")).toBeInTheDocument();
});
// Click create button
fireEvent.click(screen.getByTestId("mobile-create-roadmap-btn"));
// Should show create form
await waitFor(() => {
expect(screen.getByTestId("create-roadmap-form")).toBeInTheDocument();
expect(screen.getByTestId("create-roadmap-title")).toBeInTheDocument();
});
});
it("mobile can create roadmap via form", async () => {
const newRoadmap = {
id: "RM-003",
title: "New Roadmap",
createdAt: "2026-01-03T00:00:00.000Z",
updatedAt: "2026-01-03T00:00:00.000Z",
};
(api.createRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(newRoadmap);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-create-roadmap-btn")).toBeInTheDocument();
});
// Click create button
fireEvent.click(screen.getByTestId("mobile-create-roadmap-btn"));
// Fill in the form using userEvent for better React integration
await waitFor(() => {
const titleInput = screen.getByTestId("create-roadmap-title");
expect(titleInput).toBeInTheDocument();
});
const titleInput = screen.getByTestId("create-roadmap-title");
await userEvent.type(titleInput, "New Roadmap");
// Submit the form using fireEvent.submit
const form = screen.getByTestId("create-roadmap-form").querySelector("form");
expect(form).toBeTruthy();
fireEvent.submit(form!);
await waitFor(() => {
expect(api.createRoadmap).toHaveBeenCalledWith(
{ title: "New Roadmap" },
undefined
);
expect(mockAddToast).toHaveBeenCalledWith("Roadmap created", "success");
});
});
it("mobile edit and delete buttons are visible on roadmap items", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
});
// Edit and delete buttons should be visible (not hidden behind hover on mobile)
expect(screen.getByTestId("mobile-roadmap-edit-RM-001")).toBeInTheDocument();
expect(screen.getByTestId("mobile-roadmap-delete-RM-001")).toBeInTheDocument();
expect(screen.getByTestId("mobile-roadmap-export-RM-001")).toBeInTheDocument();
});
it("shows empty state on mobile when no roadmaps", async () => {
(api.fetchRoadmaps as ReturnType<typeof vi.fn>).mockResolvedValue([]);
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-list")).toBeInTheDocument();
});
expect(screen.getByText("No roadmaps yet.")).toBeInTheDocument();
expect(screen.getByTestId("roadmaps-view__mobile-list").textContent).toContain("Create Roadmap");
});
it("mobile header shows action buttons when roadmap selected", async () => {
render(<RoadmapsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
});
// Select a roadmap
fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
await waitFor(() => {
expect(screen.getByTestId("roadmaps-view__mobile-header")).toBeInTheDocument();
});
// Action buttons should be visible in header
expect(screen.getByTestId("mobile-header-create-btn")).toBeInTheDocument();
expect(screen.getByTestId("mobile-header-edit-btn")).toBeInTheDocument();
expect(screen.getByTestId("mobile-header-delete-btn")).toBeInTheDocument();
expect(screen.getByTestId("mobile-back-btn")).toBeInTheDocument();
});
});
});