diff --git a/apps/main/src/components/InstallBanner.test.tsx b/apps/main/src/components/InstallBanner.test.tsx new file mode 100644 index 0000000..8aaed34 --- /dev/null +++ b/apps/main/src/components/InstallBanner.test.tsx @@ -0,0 +1,128 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock the hook so we can control its return values +const mockPromptInstall = vi.fn(); +const mockDismiss = vi.fn(); + +vi.mock("../hooks/useInstallPrompt", () => ({ + useInstallPrompt: vi.fn(() => ({ + canInstall: false, + isStandalone: false, + isIOS: false, + isDismissed: false, + promptInstall: mockPromptInstall, + dismiss: mockDismiss, + })), +})); + +import { useInstallPrompt } from "../hooks/useInstallPrompt"; +import { InstallBanner } from "./InstallBanner"; + +const mockUseInstallPrompt = vi.mocked(useInstallPrompt); + +describe("InstallBanner", () => { + beforeEach(() => { + mockPromptInstall.mockClear(); + mockDismiss.mockClear(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("renders nothing when canInstall is false and not iOS", () => { + mockUseInstallPrompt.mockReturnValue({ + canInstall: false, + isStandalone: false, + isIOS: false, + isDismissed: false, + promptInstall: mockPromptInstall, + dismiss: mockDismiss, + }); + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders nothing when already in standalone mode", () => { + mockUseInstallPrompt.mockReturnValue({ + canInstall: true, + isStandalone: true, + isIOS: false, + isDismissed: false, + promptInstall: mockPromptInstall, + dismiss: mockDismiss, + }); + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders nothing when dismissed", () => { + mockUseInstallPrompt.mockReturnValue({ + canInstall: true, + isStandalone: false, + isIOS: false, + isDismissed: true, + promptInstall: mockPromptInstall, + dismiss: mockDismiss, + }); + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders install banner when canInstall is true", () => { + mockUseInstallPrompt.mockReturnValue({ + canInstall: true, + isStandalone: false, + isIOS: false, + isDismissed: false, + promptInstall: mockPromptInstall, + dismiss: mockDismiss, + }); + render(); + expect(screen.getByText(/install/i)).toBeInTheDocument(); + }); + + it("calls promptInstall when install button is clicked", async () => { + mockUseInstallPrompt.mockReturnValue({ + canInstall: true, + isStandalone: false, + isIOS: false, + isDismissed: false, + promptInstall: mockPromptInstall, + dismiss: mockDismiss, + }); + render(); + await userEvent.click(screen.getByRole("button", { name: /install/i })); + expect(mockPromptInstall).toHaveBeenCalledOnce(); + }); + + it("calls dismiss when close button is clicked", async () => { + mockUseInstallPrompt.mockReturnValue({ + canInstall: true, + isStandalone: false, + isIOS: false, + isDismissed: false, + promptInstall: mockPromptInstall, + dismiss: mockDismiss, + }); + render(); + await userEvent.click(screen.getByRole("button", { name: /dismiss|close/i })); + expect(mockDismiss).toHaveBeenCalledOnce(); + }); + + it("renders iOS instructions when isIOS is true and not dismissed", () => { + mockUseInstallPrompt.mockReturnValue({ + canInstall: false, + isStandalone: false, + isIOS: true, + isDismissed: false, + promptInstall: mockPromptInstall, + dismiss: mockDismiss, + }); + render(); + expect(screen.getByText(/share/i)).toBeInTheDocument(); + expect(screen.getByText(/add to home screen/i)).toBeInTheDocument(); + }); +}); diff --git a/apps/main/src/components/InstallBanner.tsx b/apps/main/src/components/InstallBanner.tsx new file mode 100644 index 0000000..e9ff517 --- /dev/null +++ b/apps/main/src/components/InstallBanner.tsx @@ -0,0 +1,49 @@ +import { Download, Share, X } from "lucide-react"; +import { useInstallPrompt } from "../hooks/useInstallPrompt"; + +export function InstallBanner() { + const { canInstall, isStandalone, isIOS, isDismissed, promptInstall, dismiss } = + useInstallPrompt(); + + // Don't show if already installed, dismissed, or no install option available + if (isStandalone || isDismissed) return null; + if (!canInstall && !isIOS) return null; + + return ( +
+ {isIOS ? ( + <> + +

+ Install XTablo: tap{" "} + Share then{" "} + Add to Home Screen +

+ + ) : ( + <> + +

+ Get the app for a faster, native experience +

+ + + )} + +
+ ); +}