diff --git a/src/components/__tests__/QuickstartInitialView.test.tsx b/src/components/__tests__/QuickstartInitialView.test.tsx
new file mode 100644
index 00000000..98e13193
--- /dev/null
+++ b/src/components/__tests__/QuickstartInitialView.test.tsx
@@ -0,0 +1,243 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { QuickstartInitialView } from "@/components/quickstart/QuickstartInitialView";
+
+describe("QuickstartInitialView", () => {
+ const mockOnSelectBlankCanvas = vi.fn();
+ const mockOnSelectTemplates = vi.fn();
+ const mockOnSelectVibe = vi.fn();
+ const mockOnSelectLoad = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render the Node Banana title and logo", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Node Banana")).toBeInTheDocument();
+ expect(screen.getByAltText("")).toBeInTheDocument(); // Logo image
+ });
+
+ it("should render the description text", () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByText(/node based workflow editor for AI image generation/i)
+ ).toBeInTheDocument();
+ });
+
+ it("should render all four option buttons", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Blank canvas")).toBeInTheDocument();
+ expect(screen.getByText("Load workflow")).toBeInTheDocument();
+ expect(screen.getByText("Templates")).toBeInTheDocument();
+ expect(screen.getByText("Prompt a workflow")).toBeInTheDocument();
+ });
+
+ it("should render option descriptions", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Start from scratch")).toBeInTheDocument();
+ expect(screen.getByText("Open existing file")).toBeInTheDocument();
+ expect(screen.getByText("Pre-built workflows")).toBeInTheDocument();
+ expect(screen.getByText("Describe what you want")).toBeInTheDocument();
+ });
+ });
+
+ describe("Blank Canvas Option", () => {
+ it("should call onSelectBlankCanvas when clicked", () => {
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText("Blank canvas"));
+
+ expect(mockOnSelectBlankCanvas).toHaveBeenCalledTimes(1);
+ });
+
+ it("should display correct description for blank canvas", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Start from scratch")).toBeInTheDocument();
+ });
+ });
+
+ describe("Load Workflow Option", () => {
+ it("should call onSelectLoad when clicked", () => {
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText("Load workflow"));
+
+ expect(mockOnSelectLoad).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("Templates Option", () => {
+ it("should call onSelectTemplates when clicked", () => {
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText("Templates"));
+
+ expect(mockOnSelectTemplates).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("Prompt a Workflow Option", () => {
+ it("should call onSelectVibe when clicked", () => {
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText("Prompt a workflow"));
+
+ expect(mockOnSelectVibe).toHaveBeenCalledTimes(1);
+ });
+
+ it("should display Beta badge on prompt option", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Beta")).toBeInTheDocument();
+ });
+ });
+
+ describe("External Links", () => {
+ it("should render Discord link with correct URL", () => {
+ render(
+
+ );
+
+ const discordLink = screen.getByText("Discord").closest("a");
+ expect(discordLink).toHaveAttribute(
+ "href",
+ "https://discord.com/invite/89Nr6EKkTf"
+ );
+ expect(discordLink).toHaveAttribute("target", "_blank");
+ expect(discordLink).toHaveAttribute("rel", "noopener noreferrer");
+ });
+
+ it("should render Twitter/X link with correct URL", () => {
+ render(
+
+ );
+
+ const twitterLink = screen.getByText("Willie").closest("a");
+ expect(twitterLink).toHaveAttribute("href", "https://x.com/ReflctWillie");
+ expect(twitterLink).toHaveAttribute("target", "_blank");
+ expect(twitterLink).toHaveAttribute("rel", "noopener noreferrer");
+ });
+
+ it("should render docs coming soon text (not as a link)", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Docs (coming soon)")).toBeInTheDocument();
+ expect(screen.getByText("Docs (coming soon)").closest("a")).toBeNull();
+ });
+ });
+
+ describe("Accessibility", () => {
+ it("should have all buttons as interactive button elements", () => {
+ render(
+
+ );
+
+ const buttons = screen.getAllByRole("button");
+ // Should have 4 option buttons
+ expect(buttons.length).toBe(4);
+ });
+ });
+});
diff --git a/src/components/__tests__/WelcomeModal.test.tsx b/src/components/__tests__/WelcomeModal.test.tsx
new file mode 100644
index 00000000..fbd76a80
--- /dev/null
+++ b/src/components/__tests__/WelcomeModal.test.tsx
@@ -0,0 +1,395 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
+import { WelcomeModal } from "@/components/quickstart/WelcomeModal";
+import { WorkflowFile } from "@/store/workflowStore";
+
+// Mock fetch
+const mockFetch = vi.fn();
+global.fetch = mockFetch;
+
+// Mock templates
+vi.mock("@/lib/quickstart/templates", () => ({
+ getAllPresets: () => [
+ {
+ id: "product-shot",
+ name: "Product Shot",
+ description: "Place product in a new scene or environment",
+ icon: "M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4",
+ },
+ ],
+}));
+
+describe("WelcomeModal", () => {
+ const mockOnWorkflowGenerated = vi.fn();
+ const mockOnClose = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ // Setup default fetch mock for community workflows
+ mockFetch.mockImplementation((url: string) => {
+ if (url === "/api/community-workflows") {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ success: true, workflows: [] }),
+ });
+ }
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ success: true }),
+ });
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render welcome modal with initial view by default", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Node Banana")).toBeInTheDocument();
+ expect(screen.getByText("Blank canvas")).toBeInTheDocument();
+ expect(screen.getByText("Templates")).toBeInTheDocument();
+ expect(screen.getByText("Prompt a workflow")).toBeInTheDocument();
+ });
+
+ it("should render modal overlay with backdrop", () => {
+ const { container } = render(
+
+ );
+
+ const backdrop = container.querySelector(".bg-black\\/50");
+ expect(backdrop).toBeInTheDocument();
+ });
+ });
+
+ describe("Initial View Navigation", () => {
+ it("should call onClose when 'Blank canvas' is clicked", () => {
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText("Blank canvas"));
+
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it("should navigate to templates view when 'Templates' is clicked", async () => {
+ render(
+
+ );
+
+ await act(async () => {
+ fireEvent.click(screen.getByText("Templates"));
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Workflow Templates")).toBeInTheDocument();
+ expect(screen.getByText("Quick Start")).toBeInTheDocument();
+ });
+ });
+
+ it("should navigate to vibe view when 'Prompt a workflow' is clicked", () => {
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText("Prompt a workflow"));
+
+ expect(screen.getByText("Prompt a Workflow")).toBeInTheDocument();
+ expect(screen.getByText("Describe your workflow")).toBeInTheDocument();
+ });
+ });
+
+ describe("View Transitions", () => {
+ it("should navigate back to initial view from templates view", async () => {
+ render(
+
+ );
+
+ // Navigate to templates
+ await act(async () => {
+ fireEvent.click(screen.getByText("Templates"));
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Workflow Templates")).toBeInTheDocument();
+ });
+
+ // Click back
+ await act(async () => {
+ fireEvent.click(screen.getByText("Back"));
+ });
+
+ expect(screen.getByText("Node Banana")).toBeInTheDocument();
+ expect(screen.getByText("Blank canvas")).toBeInTheDocument();
+ });
+
+ it("should navigate back to initial view from prompt view", () => {
+ render(
+
+ );
+
+ // Navigate to prompt view
+ fireEvent.click(screen.getByText("Prompt a workflow"));
+ expect(screen.getByText("Prompt a Workflow")).toBeInTheDocument();
+
+ // Click back
+ fireEvent.click(screen.getByText("Back"));
+
+ expect(screen.getByText("Node Banana")).toBeInTheDocument();
+ });
+ });
+
+ describe("File Loading", () => {
+ it("should render hidden file input for workflow loading", () => {
+ const { container } = render(
+
+ );
+
+ const fileInput = container.querySelector('input[type="file"]');
+ expect(fileInput).toBeInTheDocument();
+ expect(fileInput).toHaveAttribute("accept", ".json");
+ expect(fileInput).toHaveClass("hidden");
+ });
+
+ it("should trigger file input when 'Load workflow' is clicked", () => {
+ const { container } = render(
+
+ );
+
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
+ const clickSpy = vi.spyOn(fileInput, "click");
+
+ fireEvent.click(screen.getByText("Load workflow"));
+
+ expect(clickSpy).toHaveBeenCalled();
+ });
+
+ it("should call onWorkflowGenerated when valid workflow file is loaded", async () => {
+ const validWorkflow: WorkflowFile = {
+ id: "test-id",
+ version: 1,
+ name: "Test Workflow",
+ edgeStyle: "curved",
+ nodes: [],
+ edges: [],
+ };
+
+ // Create a mock FileReader class
+ const mockFileReader = {
+ readAsText: vi.fn(function (this: { onload: ((e: ProgressEvent) => void) | null; result: string }) {
+ setTimeout(() => {
+ this.onload?.({ target: { result: this.result } } as ProgressEvent);
+ }, 0);
+ }),
+ onload: null as ((e: ProgressEvent) => void) | null,
+ onerror: null,
+ result: JSON.stringify(validWorkflow),
+ };
+
+ vi.stubGlobal("FileReader", function FileReaderMock(this: typeof mockFileReader) {
+ Object.assign(this, mockFileReader);
+ this.readAsText = mockFileReader.readAsText.bind(this);
+ return this;
+ });
+
+ const { container } = render(
+
+ );
+
+ const file = new File([JSON.stringify(validWorkflow)], "test.json", {
+ type: "application/json",
+ });
+
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
+
+ await act(async () => {
+ fireEvent.change(fileInput, { target: { files: [file] } });
+ });
+
+ await waitFor(() => {
+ expect(mockOnWorkflowGenerated).toHaveBeenCalledWith(validWorkflow);
+ });
+
+ vi.unstubAllGlobals();
+ });
+
+ it("should show alert for invalid workflow file format", async () => {
+ const alertSpy = vi.spyOn(window, "alert").mockImplementation(() => {});
+
+ const invalidWorkflow = { foo: "bar" }; // Missing required fields
+
+ const mockFileReader = {
+ readAsText: vi.fn(function (this: { onload: ((e: ProgressEvent) => void) | null; result: string }) {
+ setTimeout(() => {
+ this.onload?.({ target: { result: this.result } } as ProgressEvent);
+ }, 0);
+ }),
+ onload: null as ((e: ProgressEvent) => void) | null,
+ onerror: null,
+ result: JSON.stringify(invalidWorkflow),
+ };
+
+ vi.stubGlobal("FileReader", function FileReaderMock(this: typeof mockFileReader) {
+ Object.assign(this, mockFileReader);
+ this.readAsText = mockFileReader.readAsText.bind(this);
+ return this;
+ });
+
+ const { container } = render(
+
+ );
+
+ const file = new File([JSON.stringify(invalidWorkflow)], "test.json", {
+ type: "application/json",
+ });
+
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
+
+ await act(async () => {
+ fireEvent.change(fileInput, { target: { files: [file] } });
+ });
+
+ await waitFor(() => {
+ expect(alertSpy).toHaveBeenCalledWith("Invalid workflow file format");
+ });
+ expect(mockOnWorkflowGenerated).not.toHaveBeenCalled();
+
+ vi.unstubAllGlobals();
+ });
+
+ it("should show alert when file parsing fails", async () => {
+ const alertSpy = vi.spyOn(window, "alert").mockImplementation(() => {});
+
+ const mockFileReader = {
+ readAsText: vi.fn(function (this: { onload: ((e: ProgressEvent) => void) | null; result: string }) {
+ setTimeout(() => {
+ this.onload?.({ target: { result: this.result } } as ProgressEvent);
+ }, 0);
+ }),
+ onload: null as ((e: ProgressEvent) => void) | null,
+ onerror: null,
+ result: "invalid json",
+ };
+
+ vi.stubGlobal("FileReader", function FileReaderMock(this: typeof mockFileReader) {
+ Object.assign(this, mockFileReader);
+ this.readAsText = mockFileReader.readAsText.bind(this);
+ return this;
+ });
+
+ const { container } = render(
+
+ );
+
+ const file = new File(["invalid json"], "test.json", {
+ type: "application/json",
+ });
+
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
+
+ await act(async () => {
+ fireEvent.change(fileInput, { target: { files: [file] } });
+ });
+
+ await waitFor(() => {
+ expect(alertSpy).toHaveBeenCalledWith("Failed to parse workflow file");
+ });
+
+ vi.unstubAllGlobals();
+ });
+
+ it("should not process if no file is selected", () => {
+ const { container } = render(
+
+ );
+
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
+
+ fireEvent.change(fileInput, { target: { files: [] } });
+
+ expect(mockOnWorkflowGenerated).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("Workflow Selection from Child Views", () => {
+ it("should call onWorkflowGenerated when workflow is generated from templates view", async () => {
+ render(
+
+ );
+
+ // Navigate to templates
+ await act(async () => {
+ fireEvent.click(screen.getByText("Templates"));
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Workflow Templates")).toBeInTheDocument();
+ });
+
+ // Verify templates view is showing - the actual workflow selection is tested in QuickstartTemplatesView tests
+ expect(screen.getByText("Quick Start")).toBeInTheDocument();
+ });
+
+ it("should show prompt view when navigating to vibe", () => {
+ render(
+
+ );
+
+ // Navigate to vibe/prompt view
+ fireEvent.click(screen.getByText("Prompt a workflow"));
+
+ expect(screen.getByText("Prompt a Workflow")).toBeInTheDocument();
+ expect(screen.getByText("Generate Workflow")).toBeInTheDocument();
+ });
+ });
+});