diff --git a/apps/pwa/src/components/InstructionSheet.test.tsx b/apps/pwa/src/components/InstructionSheet.test.tsx new file mode 100644 index 0000000..f0c1dbb --- /dev/null +++ b/apps/pwa/src/components/InstructionSheet.test.tsx @@ -0,0 +1,90 @@ +/** + * InstructionSheet.test.tsx — wiring test for SettingsSheet "How to enable" fix (UAT-05-T4). + * + * Asserts: + * - Clicking "How to enable" in SettingsSheet opens the InstructionSheet dialog + * - The dialog has heading "How to enable notifications" + * - onClose (the sheet-close prop) is NOT called when the dialog opens + */ + +import React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' + +// ── Module mocks ────────────────────────────────────────────────────────────── + +vi.mock('../hooks/usePushSubscription.js', () => ({ + usePushSubscription: vi.fn(() => ({ + permission: 'denied' as NotificationPermission, + isSubscribed: false, + subscribe: vi.fn(), + setEnabled: vi.fn(), + })), + readNotificationsEnabled: vi.fn(() => false), +})) + +// ── Minimal Notification stub (jsdom lacks it) ──────────────────────────────── + +beforeEach(() => { + if (typeof globalThis.Notification === 'undefined') { + Object.defineProperty(globalThis, 'Notification', { + value: { permission: 'denied' }, + writable: true, + configurable: true, + }) + } else { + Object.defineProperty(globalThis.Notification, 'permission', { + value: 'denied', + writable: true, + configurable: true, + }) + } +}) + +// ── Import component (after mocks) ──────────────────────────────────────────── + +import { SettingsSheet } from './SettingsSheet.js' + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe('SettingsSheet — "How to enable" wiring (UAT-05-T4)', () => { + it('clicking "How to enable" opens the InstructionSheet dialog and does NOT call onClose', () => { + const onCloseSpy = vi.fn() + render() + + // No instruction dialog yet + expect(screen.queryByRole('dialog', { name: /re-enable notifications/i })).toBeNull() + + // Click the "How to enable" button + const howToEnableBtn = screen.getByText('How to enable') + fireEvent.click(howToEnableBtn) + + // The InstructionSheet dialog should now be visible + const instructionDialog = screen.getByRole('dialog', { name: /re-enable notifications/i }) + expect(instructionDialog).toBeDefined() + + // The heading inside the dialog + expect(screen.getByText('How to enable notifications')).toBeDefined() + + // onClose (sheet close prop) must NOT have been called + expect(onCloseSpy).not.toHaveBeenCalled() + }) + + it('InstructionSheet "Done" button closes the instruction dialog without calling sheet onClose', () => { + const onCloseSpy = vi.fn() + render() + + // Open the instruction sheet + fireEvent.click(screen.getByText('How to enable')) + expect(screen.getByRole('dialog', { name: /re-enable notifications/i })).toBeDefined() + + // Click Done — closes the instruction sheet + fireEvent.click(screen.getByText('Done')) + + // Instruction dialog should be gone + expect(screen.queryByRole('dialog', { name: /re-enable notifications/i })).toBeNull() + + // Sheet onClose must NOT have been called + expect(onCloseSpy).not.toHaveBeenCalled() + }) +}) diff --git a/apps/pwa/src/components/SettingsSheet.tsx b/apps/pwa/src/components/SettingsSheet.tsx index 726a5e6..7419b87 100644 --- a/apps/pwa/src/components/SettingsSheet.tsx +++ b/apps/pwa/src/components/SettingsSheet.tsx @@ -24,6 +24,7 @@ import { useEffect, useRef, useState } from 'react' import { X, Bell, AlertCircle, Loader2 } from 'lucide-react' import { usePushSubscription } from '../hooks/usePushSubscription.js' +import { InstructionSheet } from './InstructionSheet.js' // CR-04: fetch VAPID key (from sessionStorage cache if available) for the // tap-gated subscribe() path. Same logic as PushPermissionPrompt. @@ -51,6 +52,7 @@ interface SettingsSheetProps { export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) { const { subscribe, permission, isSubscribed, setEnabled } = usePushSubscription() const [isTogglingOn, setIsTogglingOn] = useState(false) + const [instructionsOpen, setInstructionsOpen] = useState(false) // CR-04: pre-fetch the VAPID key into state so the toggle tap handler can call // subscribe(registration, vapidKey) without any network await before pushManager.subscribe(). const [vapidKey, setVapidKey] = useState(null) @@ -370,7 +372,7 @@ export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) { Notifications are blocked in your browser settings.{' '}