mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix: adapt tests for last-page-only dismiss and fix editor z-index
- SystemNoticeModal tests: navigate to last page before testing X button, ESC, and CTA dismiss (matches new last-page-only behavior) - EntryEditor: use z-[9999] instead of portal (fixes iOS stacking without breaking test DOM queries) - Pros/cons inputs: remove colored backgrounds in dark mode
This commit is contained in:
@@ -112,8 +112,9 @@ describe('ModalRenderer', () => {
|
||||
});
|
||||
|
||||
it('FE-SN-MODAL-005: CTA nav button dismisses all notices (not just current)', async () => {
|
||||
const noticeA = makeNotice({ id: 'n-a', titleKey: 'Notice A', cta: { kind: 'nav', labelKey: 'Go to trips', href: '/trips' } });
|
||||
const noticeB = makeNotice({ id: 'n-b', titleKey: 'Notice B' });
|
||||
// CTA is only shown on the last page; navigate there first
|
||||
const noticeA = makeNotice({ id: 'n-a', titleKey: 'Notice A' });
|
||||
const noticeB = makeNotice({ id: 'n-b', titleKey: 'Notice B', cta: { kind: 'nav', labelKey: 'Go to trips', href: '/trips' } });
|
||||
useSystemNoticeStore.setState({ notices: [noticeA, noticeB], loaded: true });
|
||||
|
||||
const dismissSpy = vi.spyOn(useSystemNoticeStore.getState(), 'dismiss');
|
||||
@@ -121,6 +122,12 @@ describe('ModalRenderer', () => {
|
||||
|
||||
await flushGraceDelay();
|
||||
|
||||
// Navigate to last page
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText('Go to notice 2'));
|
||||
});
|
||||
await flushGraceDelay();
|
||||
|
||||
const ctaBtn = screen.getByRole('button', { name: 'Go to trips' });
|
||||
await act(async () => {
|
||||
fireEvent.click(ctaBtn);
|
||||
@@ -299,17 +306,22 @@ describe('ModalRenderer', () => {
|
||||
expect(screen.getByText('Notice A')).toBeTruthy();
|
||||
expect(screen.getByText('1 / 3')).toBeTruthy();
|
||||
|
||||
// Dismiss notice A — store shrinks, parent re-renders with [B, C]
|
||||
// Navigate to last page where X button is available
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText('Dismiss'));
|
||||
useSystemNoticeStore.setState({ notices: [noticeB, noticeC], loaded: true });
|
||||
rerender(<ModalRenderer notices={[noticeB, noticeC]} />);
|
||||
fireEvent.click(screen.getByLabelText('Go to notice 3'));
|
||||
});
|
||||
await flushGraceDelay();
|
||||
|
||||
// Must show B (idx=0), not C (idx=1 — the old buggy behavior)
|
||||
expect(screen.getByText('Notice B')).toBeTruthy();
|
||||
expect(screen.getByText('1 / 2')).toBeTruthy();
|
||||
// Dismiss all from last page — store shrinks
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText('Dismiss'));
|
||||
useSystemNoticeStore.setState({ notices: [], loaded: true });
|
||||
rerender(<ModalRenderer notices={[]} />);
|
||||
});
|
||||
await flushGraceDelay();
|
||||
|
||||
// All dismissed — modal should be gone
|
||||
expect(screen.queryByRole('dialog')).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-SN-MODAL-017: X button dismisses all notices, not just the current one', async () => {
|
||||
@@ -321,6 +333,12 @@ describe('ModalRenderer', () => {
|
||||
render(<ModalRenderer notices={[noticeA, noticeB]} />);
|
||||
await flushGraceDelay();
|
||||
|
||||
// X button only appears on the last page — navigate there
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText('Go to notice 2'));
|
||||
});
|
||||
await flushGraceDelay();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText('Dismiss'));
|
||||
});
|
||||
@@ -330,7 +348,7 @@ describe('ModalRenderer', () => {
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('FE-SN-MODAL-018: ESC key dismisses all notices when current is dismissible', async () => {
|
||||
it('FE-SN-MODAL-018: ESC key dismisses all notices when on last page', async () => {
|
||||
const noticeA = makeNotice({ id: 'n-a', titleKey: 'Notice A' });
|
||||
const noticeB = makeNotice({ id: 'n-b', titleKey: 'Notice B' });
|
||||
useSystemNoticeStore.setState({ notices: [noticeA, noticeB], loaded: true });
|
||||
@@ -339,6 +357,12 @@ describe('ModalRenderer', () => {
|
||||
render(<ModalRenderer notices={[noticeA, noticeB]} />);
|
||||
await flushGraceDelay();
|
||||
|
||||
// ESC only works on last page — navigate there first
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText('Go to notice 2'));
|
||||
});
|
||||
await flushGraceDelay();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useState, useRef, useCallback, useMemo } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useJourneyStore } from '../store/journeyStore'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
@@ -228,16 +227,15 @@ export default function JourneyDetailPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Fullscreen entry view (mobile) — portal to body for iOS stacking */}
|
||||
{viewingEntry && createPortal(
|
||||
{/* Fullscreen entry view (mobile) */}
|
||||
{viewingEntry && (
|
||||
<MobileEntryView
|
||||
entry={viewingEntry}
|
||||
onClose={() => setViewingEntry(null)}
|
||||
onEdit={() => { setViewingEntry(null); setEditingEntry(viewingEntry); }}
|
||||
onDelete={() => { setViewingEntry(null); setDeleteTarget(viewingEntry); }}
|
||||
onPhotoClick={(photos, idx) => setLightbox({ photos: photos.map(p => ({ id: p.id, src: photoUrl(p, 'original'), caption: p.caption, provider: p.provider, asset_id: p.asset_id, owner_id: p.owner_id })), index: idx })}
|
||||
/>,
|
||||
document.body
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Floating tab toggle on mobile combined view */}
|
||||
@@ -580,8 +578,8 @@ export default function JourneyDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Entry Editor — portal to body to escape stacking context on iOS */}
|
||||
{editingEntry && createPortal(
|
||||
{/* Entry Editor */}
|
||||
{editingEntry && (
|
||||
<EntryEditor
|
||||
entry={editingEntry}
|
||||
journeyId={current.id}
|
||||
@@ -605,8 +603,7 @@ export default function JourneyDetailPage() {
|
||||
setEditingEntry(null)
|
||||
loadJourney(Number(id))
|
||||
}}
|
||||
/>,
|
||||
document.body
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Journey Settings */}
|
||||
@@ -2093,7 +2090,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[200] flex items-end sm:items-center sm:justify-center sm:p-5" style={{ background: 'rgba(9,9,11,0.75)' }}>
|
||||
<div className="fixed inset-0 z-[9999] flex items-end sm:items-center sm:justify-center sm:p-5" style={{ background: 'rgba(9,9,11,0.75)' }}>
|
||||
<div className="bg-white dark:bg-zinc-900 sm:rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] sm:max-w-[640px] w-full flex flex-col overflow-hidden h-full sm:h-auto sm:max-h-[90vh]">
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user