/** * RED test scaffold: components/InstallPrompt.tsx — iOS/Android install prompt (PWA-01, PWA-02) * * Behaviors under test: * 1. isIOSSafariNonStandalone() returns true for a mock iOS Safari non-standalone UA * 2. isIOSSafariNonStandalone() returns false when running in standalone mode * 3. useAndroidInstallPrompt sets canInstall=true when a mock beforeinstallprompt event fires * 4. useAndroidInstallPrompt sets canInstall=false when appinstalled event fires * * These tests FAIL (RED) because components/InstallPrompt.tsx does not exist yet. * They will turn GREEN in Plan 03-06 when the implementation is added. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, act } from '@testing-library/react'; // This import fails (RED) — InstallPrompt.tsx does not exist yet. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore intentional RED import import { isIOSSafariNonStandalone, useAndroidInstallPrompt } from './InstallPrompt.js'; // Capture the original navigator descriptors so we can restore them const originalUserAgent = navigator.userAgent; function setUserAgent(ua: string) { Object.defineProperty(navigator, 'userAgent', { value: ua, writable: true, configurable: true, }); } function setStandalone(value: boolean) { // navigator.standalone is an iOS-only property Object.defineProperty(navigator, 'standalone', { value, writable: true, configurable: true, }); } describe('isIOSSafariNonStandalone', () => { afterEach(() => { // Restore original userAgent Object.defineProperty(navigator, 'userAgent', { value: originalUserAgent, writable: true, configurable: true, }); // Remove standalone mock Object.defineProperty(navigator, 'standalone', { value: undefined, writable: true, configurable: true, }); }); it('returns true for an iOS Safari non-standalone UA', () => { setUserAgent( 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_4 like Mac OS X) ' + 'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1', ); setStandalone(false); expect(isIOSSafariNonStandalone()).toBe(true); }); it('returns false when running in standalone mode (PWA installed)', () => { setUserAgent( 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_4 like Mac OS X) ' + 'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1', ); setStandalone(true); expect(isIOSSafariNonStandalone()).toBe(false); }); it('returns false for an Android Chrome UA', () => { setUserAgent( 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36', ); setStandalone(false); expect(isIOSSafariNonStandalone()).toBe(false); }); }); describe('useAndroidInstallPrompt', () => { beforeEach(() => { vi.clearAllMocks(); }); it('sets canInstall=true when a beforeinstallprompt event is dispatched', () => { const { result } = renderHook(() => useAndroidInstallPrompt()); expect(result.current.canInstall).toBe(false); const mockPromptEvent = new Event('beforeinstallprompt') as Event & { prompt: () => Promise; userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>; }; mockPromptEvent.prompt = vi.fn().mockResolvedValue(undefined); mockPromptEvent.userChoice = Promise.resolve({ outcome: 'accepted' }); act(() => { window.dispatchEvent(mockPromptEvent); }); expect(result.current.canInstall).toBe(true); }); it('sets canInstall=false when appinstalled event fires', () => { const { result } = renderHook(() => useAndroidInstallPrompt()); // First fire beforeinstallprompt to set canInstall=true const mockPromptEvent = new Event('beforeinstallprompt') as Event & { prompt: () => Promise; userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>; }; mockPromptEvent.prompt = vi.fn().mockResolvedValue(undefined); mockPromptEvent.userChoice = Promise.resolve({ outcome: 'dismissed' }); act(() => { window.dispatchEvent(mockPromptEvent); }); expect(result.current.canInstall).toBe(true); // Now fire appinstalled — should reset canInstall to false act(() => { window.dispatchEvent(new Event('appinstalled')); }); expect(result.current.canInstall).toBe(false); }); });