fix(quick-260610-jlp-01): wire SettingsSheet 'How to enable' to open InstructionSheet
- Add instructionsOpen state to SettingsSheet
- Change broken onClick={onClose} to onClick={() => setInstructionsOpen(true)}
- Render InstructionSheet conditionally when instructionsOpen=true
- Add InstructionSheet.test.tsx: asserts dialog opens + onClose not called (UAT-05-T4)
This commit is contained in:
@@ -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(<SettingsSheet isOpen={true} onClose={onCloseSpy} />)
|
||||||
|
|
||||||
|
// 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(<SettingsSheet isOpen={true} onClose={onCloseSpy} />)
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { X, Bell, AlertCircle, Loader2 } from 'lucide-react'
|
import { X, Bell, AlertCircle, Loader2 } from 'lucide-react'
|
||||||
import { usePushSubscription } from '../hooks/usePushSubscription.js'
|
import { usePushSubscription } from '../hooks/usePushSubscription.js'
|
||||||
|
import { InstructionSheet } from './InstructionSheet.js'
|
||||||
|
|
||||||
// CR-04: fetch VAPID key (from sessionStorage cache if available) for the
|
// CR-04: fetch VAPID key (from sessionStorage cache if available) for the
|
||||||
// tap-gated subscribe() path. Same logic as PushPermissionPrompt.
|
// tap-gated subscribe() path. Same logic as PushPermissionPrompt.
|
||||||
@@ -51,6 +52,7 @@ interface SettingsSheetProps {
|
|||||||
export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
||||||
const { subscribe, permission, isSubscribed, setEnabled } = usePushSubscription()
|
const { subscribe, permission, isSubscribed, setEnabled } = usePushSubscription()
|
||||||
const [isTogglingOn, setIsTogglingOn] = useState(false)
|
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
|
// 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().
|
// subscribe(registration, vapidKey) without any network await before pushManager.subscribe().
|
||||||
const [vapidKey, setVapidKey] = useState<string | null>(null)
|
const [vapidKey, setVapidKey] = useState<string | null>(null)
|
||||||
@@ -370,7 +372,7 @@ export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
|||||||
Notifications are blocked in your browser settings.{' '}
|
Notifications are blocked in your browser settings.{' '}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={() => setInstructionsOpen(true)}
|
||||||
style={{
|
style={{
|
||||||
background: 'none',
|
background: 'none',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@@ -388,6 +390,10 @@ export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{instructionsOpen && (
|
||||||
|
<InstructionSheet onClose={() => setInstructionsOpen(false)} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user