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.{' '}